├── .gitignore ├── QuadCubeMesh.m ├── DodecahedronMesh.m ├── IcosahedronMesh.m ├── ProjectOnSn.m ├── QuadRhombDodecMesh.m ├── LICENSE.md ├── ClosedMeshVolume.m ├── RandSampleSphere.m ├── GetMeshData.m ├── SubdivideSphericalMesh.m ├── SpiralSampleSphere.m ├── QuadQuad.m ├── TriQuad.m ├── README.md └── ParticleSampleSphere.m /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.asv 3 | -------------------------------------------------------------------------------- /QuadCubeMesh.m: -------------------------------------------------------------------------------- 1 | function fv=QuadCubeMesh 2 | % Get a quad mesh of a cube whose vertices lie on the surface of a 3 | % zero-centered unit sphere. 4 | % 5 | % - fv : structure with fields 'faces' and 'vertices' representing 6 | % quad mesh of a cube 7 | % 8 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 9 | % 10 | 11 | X=[1 1; -1 1; -1 -1; 1 -1]; 12 | [Xd,Xu]=deal(X); 13 | Xd(:,3)=-1; 14 | Xu(:,3)=1; 15 | X=[Xd;Xu]; 16 | 17 | F=[1 5 8 4; ... 18 | 2 6 5 1; ... 19 | 3 7 6 2; ... 20 | 4 8 7 3; ... 21 | 5 6 7 8; ... 22 | 1 4 3 2]; 23 | 24 | fv.faces=F; 25 | fv.vertices=X/sqrt(3); 26 | -------------------------------------------------------------------------------- /DodecahedronMesh.m: -------------------------------------------------------------------------------- 1 | function TR=DodecahedronMesh 2 | % Generate a triangular surface mesh of a Pentakis dodecahedron. This 3 | % object is a dual representation of an icosahedron; i.e., its vertices 4 | % correspond to the centroids of icosahedron's faces. 5 | % 6 | % - TR : 'triangulation' object representing a Pentakis dodecahedron. 7 | % 8 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 9 | % 10 | 11 | 12 | tr=IcosahedronMesh; 13 | X=tr.Points; 14 | F=tr.ConnectivityList; 15 | 16 | C=(X(F(:,1),:)+X(F(:,2),:)+X(F(:,3),:))/3; 17 | C=ProjectOnSn(C); 18 | 19 | X=[X;C]; 20 | 21 | Tri=convhull(X); 22 | if ClosedMeshVolume({Tri X})<0, Tri=fliplr(Tri); end 23 | TR=triangulation(Tri,X); 24 | 25 | -------------------------------------------------------------------------------- /IcosahedronMesh.m: -------------------------------------------------------------------------------- 1 | function TR=IcosahedronMesh 2 | % Generate a triangular surface mesh of an icosahedron whose vertices lie 3 | % on the surface of a zero-centered unit sphere. 4 | % 5 | % OUTPUT: 6 | % - TR : 'triangulation' object representing a zero-centered regular 7 | % icosahedron. This object has 12 evenly distributed vertices 8 | % and 20 triangular faces. 9 | % 10 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 11 | % 12 | 13 | 14 | % Vertex coordinates 15 | t=(1+sqrt(5))/2; % golden ratio 16 | X=[0 1 t]; 17 | s=[1 1 1; 1 1 -1; 1 -1 -1; 1 -1 1]; 18 | X=repmat(X,[4 1]).*s; 19 | X=[X;circshift(X,[0 -1]);circshift(X,[0 -2])]; 20 | X=ProjectOnSn(X); 21 | 22 | % Triangulate the points 23 | Tri=convhull(X); 24 | if ClosedMeshVolume({Tri X})<0, Tri=fliplr(Tri); end 25 | TR=triangulation(Tri,X); 26 | -------------------------------------------------------------------------------- /ProjectOnSn.m: -------------------------------------------------------------------------------- 1 | function X=ProjectOnSn(X) 2 | % Project a set of points onto Sn. 3 | % 4 | % INPUT: 5 | % - X : N-by-d array of point coordinates, where N is the number of 6 | % points. 7 | % 8 | % OUTPUT: 9 | % - X : N-by-d array of point coordinates such that norm(X(i,:))=1 10 | % 11 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 12 | % 13 | 14 | 15 | if ismatrix(X) && isnumeric(X) 16 | X=bsxfun(@rdivide,X,sqrt(sum(X.^2,2))); 17 | else 18 | try 19 | [Tri,X,fmt]=GetMeshData(X); 20 | catch 21 | error('Invalid entry for 1st input argument (X)') 22 | end 23 | 24 | X=bsxfun(@rdivide,X,sqrt(sum(X.^2,2))); 25 | 26 | if fmt==1 27 | X=triangulation(Tri,X); 28 | elseif fmt==2 29 | X=TriRep(Tri,X); %#ok<*DTRIREP> 30 | elseif fmt==3 31 | X={Tri X}; 32 | else 33 | X=struct('faces',Tri,'vertices',X); 34 | end 35 | end 36 | 37 | -------------------------------------------------------------------------------- /QuadRhombDodecMesh.m: -------------------------------------------------------------------------------- 1 | function fv=QuadRhombDodecMesh 2 | % Get a quad mesh of a rhombic dodecahedron whose vertices lie on the 3 | % surface of a zero-centered unit sphere. 4 | % 5 | % - fv : structure with fields 'faces' and 'vertices' representing 6 | % quad mesh of a rhombic dodecahedron 7 | % 8 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 9 | % 10 | 11 | 12 | a=sqrt(2); 13 | b=sqrt(2)/2; 14 | 15 | X=[ 0, 0, a;... 16 | 0, 0, -a;... 17 | a, 0, 0;... 18 | -a, 0, 0;... 19 | 0, a, 0;... 20 | 0, -a, 0;... 21 | b, b, b;... 22 | b, b, -b;... 23 | b, -b, b;... 24 | b, -b, -b;... 25 | -b, b, b;... 26 | -b, b, -b;... 27 | -b, -b, b;... 28 | -b, -b, -b]; 29 | 30 | F=[ 7, 1, 9, 3;... 31 | 7, 3, 8, 5;... 32 | 7, 5, 11, 1;... 33 | 10, 2, 8, 3;... 34 | 10, 3, 9, 6;... 35 | 10, 6, 14, 2;... 36 | 12, 2, 14, 4;... 37 | 12, 4, 11, 5;... 38 | 12, 5, 8, 2;... 39 | 13, 1, 11, 4;... 40 | 13, 4, 14, 6;... 41 | 13, 6, 9, 1]; 42 | 43 | fv.faces=F; 44 | fv.vertices=X/sqrt(3); 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Anton Semechko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /ClosedMeshVolume.m: -------------------------------------------------------------------------------- 1 | function Vol=ClosedMeshVolume(TR) 2 | % Compute volume of a region enclosed by a triangular surface mesh. 3 | % 4 | % INPUT: 5 | % - TR : input surface mesh represented as an object of 'TriRep' 6 | % class, 'triangulation' class, or a cell such that TR={Tri,V}, 7 | % where Tri is an M-by-3 array of faces and V is an N-by-3 8 | % array of vertex coordinates. 9 | % 10 | % OUTPUT: 11 | % - Vol : real number specifying volume enclosed by TR. Vol<0 means 12 | % that the order of mesh vertices in the face connectivity list 13 | % is in the clockwise direction, as viewed from the outside the 14 | % mesh. 15 | % 16 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 17 | % 18 | 19 | 20 | % Face and vertex lists 21 | [Tri,V]=GetMeshData(TR); 22 | if size(Tri,2)~=3 23 | error('This function is intended ONLY for triangular surface meshes') 24 | end 25 | 26 | % Face vertices 27 | V1=V(Tri(:,1),:); 28 | V2=V(Tri(:,2),:); 29 | V3=V(Tri(:,3),:); 30 | 31 | % Face centroids 32 | C=(V1+V2+V3)/3; 33 | 34 | % Face normals 35 | FN=cross(V2-V1,V3-V1,2); 36 | 37 | % Volume 38 | Vol=sum(dot(C,FN,2))/6; 39 | 40 | -------------------------------------------------------------------------------- /RandSampleSphere.m: -------------------------------------------------------------------------------- 1 | function X=RandSampleSphere(N,spl) 2 | % Generate a uniform or stratified sampling of a unit sphere. 3 | % 4 | % INPUTS: 5 | % - N : desired number of point samples. N=200 is default. 6 | % - spl : can be 'uniform' or 'stratified'. The former setting is used by 7 | % default. 8 | % 9 | % OUTPUT: 10 | % - X : N-by-3 array of sample point coordinates. 11 | % 12 | % REFERENCE: 13 | % - Shao & Badler, 1996, Spherical Sampling by Archimedes' Theorem 14 | % 15 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 16 | % 17 | 18 | 19 | % Default arguments 20 | if nargin<1 || isempty(N), N=200; end 21 | if nargin<2 || isempty(spl), spl='uniform'; end 22 | 23 | % Basic error checking 24 | chk=strcmpi(spl,{'uniform','stratified'}); 25 | if sum(chk)==0 26 | error('Invalid sampling option') 27 | end 28 | 29 | N=round(N); 30 | if numel(N)~=1 || ~isnumeric(N) || N<1 31 | error('Invalid entry for 1st input argument (N)') 32 | end 33 | if N<3, spl='uniform'; end 34 | 35 | % Sample the unfolded right cylinder 36 | if strcmp(spl,'stratified') 37 | 38 | % Partition the [-1,1]x[0,2*pi] domain into ceil(sqrt(N))^2 subdomains 39 | % and then draw a random sample for each 40 | n=ceil(sqrt(N)); 41 | ds=2/n; 42 | [Xc,Yc]=meshgrid((-1+ds/2):ds:(1-ds/2)); 43 | 44 | x=ds*(rand(n^2,1)-0.5); 45 | y=ds*(rand(n^2,1)-0.5); 46 | 47 | x=x+Xc(:); 48 | y=y+Yc(:); 49 | clear Xc Yc 50 | 51 | % Remove excess samples 52 | R=n^2-N; 53 | if R>0 54 | idx=randperm(n^2,R); 55 | x(idx)=[]; 56 | y(idx)=[]; 57 | end 58 | 59 | lon=(x+1)*pi; 60 | z=y; 61 | 62 | else 63 | z=2*rand(N,1)-1; 64 | lon=2*pi*rand(N,1); 65 | end 66 | 67 | % Convert z to latitude 68 | z(z<-1)=-1; 69 | z(z>1)=1; 70 | lat=acos(z); 71 | 72 | % Convert spherical to rectangular co-ords 73 | s=sin(lat); 74 | x=cos(lon).*s; 75 | y=sin(lon).*s; 76 | 77 | X=[x,y,z]; 78 | 79 | -------------------------------------------------------------------------------- /GetMeshData.m: -------------------------------------------------------------------------------- 1 | function [Tri,X,fmt]=GetMeshData(TR) 2 | % Get face-vertex connectivity list and list of vertex coordinates of a 3 | % mesh (TR) represented in one of the following formats: 4 | % 5 | % (1) 'triangulation' object, 6 | % (2) 'TriRep' object, 7 | % (3) 1-by-2 cell such that TR={Tri,V}, where Tri is a M-by-3 or M-by-4 8 | % array of elements and V is a N-by-3 array of vertex coordinates, or 9 | % (4) structure with fields 'faces' and 'vertices', such that 10 | % Tri=TR.faces, and V=TR.vertices, where Tri and V have the same 11 | % meaning as in (3). 12 | % 13 | % OUTPUT: 14 | % - Tri : M-by-3 array of face-vertex connectivities when TR is 15 | % triangular mesh, and M-by-4 array when TR is a quad or tet 16 | % mesh. 17 | % - X : N-by-3 array of vertex coordinates 18 | % - fmt : mesh format (see above) 19 | % 20 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 21 | % 22 | 23 | 24 | % Get face and vertex lists 25 | if isa(TR,'triangulation') 26 | Tri=TR.ConnectivityList; 27 | X=TR.Points; 28 | fmt=1; 29 | elseif isa(TR,'TriRep') 30 | Tri=TR.Triangulation; 31 | X=TR.X; 32 | fmt=2; 33 | elseif iscell(TR) && numel(TR)==2 34 | Tri=TR{1}; 35 | X=TR{2}; 36 | fmt=3; 37 | elseif isstruct(TR) && isfield(TR,'faces') && isfield(TR,'vertices') 38 | Tri=TR.faces; 39 | X=TR.vertices; 40 | fmt=4; 41 | else 42 | error('Unrecognized mesh format') 43 | end 44 | if fmt<3, return; end 45 | 46 | % Check that face and vertex lists have correct dimensions 47 | c=size(Tri,2); 48 | if ~isnumeric(Tri) || ~ismatrix(Tri) || c<3 || c>4 || ~isequal(Tri,round(Tri)) || any(Tri(:)<0) 49 | error('Vertex connectivity list must be specified as a M-by-(3 or 4) array of POSITIVE integers') 50 | end 51 | 52 | d=size(X,2); 53 | if ~isnumeric(X) || ~ismatrix(X) || any(~isfinite(X(:))) || d<2 || d>3 54 | error('List of vertex coordinates must be specified as a N-by-2 or N-by-3 array of real numbers') 55 | end 56 | 57 | -------------------------------------------------------------------------------- /SubdivideSphericalMesh.m: -------------------------------------------------------------------------------- 1 | function [TR,G]=SubdivideSphericalMesh(TR,k,G,W) 2 | % Subdivide triangular (or quadrilateral) surface mesh, representing 3 | % a zero-centered unit sphere, k times using triangular (or quadrilateral) 4 | % quadrisection. See function 'TriQuad' (or 'QuadQuad') for more info. 5 | % 6 | % INPUT: 7 | % - TR : surface mesh of a unit sphere represented as an object of 8 | % 'TriRep' class, 'triangulation' class, or a cell such that 9 | % TR={F,X}, where F is an M-by-3 or M-by-4 array of faces, 10 | % and X is an N-by-3 array of vertex coordinates. 11 | % - k : desired number of subdivisions. k=1 is default. 12 | % - G : optional; scalar or vector field defined at the vertices of TR. 13 | % - W : optional; positive weights associated with vertices of TR. 14 | % See function 'TriQuad' (or 'QuadQuad') for more info. 15 | % 16 | % OUTPUT: 17 | % - TR : subdivided mesh. Same format as input mesh. 18 | % 19 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 20 | % 21 | 22 | 23 | if nargin<2 || isempty(k), k=1; end 24 | if nargin<3, G=[]; end 25 | if nargin<4, W=[]; end 26 | 27 | 28 | % Get the data structure 29 | [F,X,fmt]=GetMeshData(TR); 30 | if fmt==1 && size(F,2)==4 31 | error('Tet meshes cannot be processed by this function') 32 | end 33 | 34 | % Make sure vertices of the input mesh lie on a unit sphere 35 | X=ProjectOnSn(X); 36 | 37 | % Return mesh as is if k<1 38 | k=round(k(1)); 39 | if k<1 40 | TR=MeshOut(F,X,fmt); 41 | return 42 | end 43 | 44 | % Spherical subdivision 45 | for i=1:k 46 | 47 | % Subdivide mesh 48 | if size(F,2)==3 49 | [TR,W,G]=TriQuad({F X},W,G); 50 | else 51 | [TR,W,G]=QuadQuad({F X},W,G); 52 | end 53 | N1=size(X,1); 54 | [F,X]=deal(TR{1},TR{2}); 55 | 56 | % Project vertices onto unit sphere 57 | N1=N1+1; 58 | N2=size(X,1); 59 | X(N1:N2,:)=ProjectOnSn(X(N1:N2,:)); 60 | 61 | end 62 | TR=MeshOut(F,X,fmt); 63 | 64 | 65 | function TR=MeshOut(F,X,fmt) 66 | 67 | switch fmt 68 | case 1 69 | TR=triangulation(F,X); 70 | case 2 71 | TR=TriRep(F,X); %#ok<*DTRIREP> 72 | case 3 73 | TR={F X}; 74 | case 4 75 | TR=struct('faces',F,'vertices',X); 76 | end 77 | 78 | -------------------------------------------------------------------------------- /SpiralSampleSphere.m: -------------------------------------------------------------------------------- 1 | function [V,Tri]=SpiralSampleSphere(N,vis) 2 | % Produce an approximately uniform distribution of points on a unit sphere 3 | % by sampling along a spherical spiral [1]. According to this approach 4 | % particle (i.e., sample) longitudes are proportional to particle rank 5 | % (1 to N) and latitudes are assigned to ensure uniform sampling density. 6 | % 7 | % INPUT: 8 | % - N : desired number of particles. N=200 is the default setting. 9 | % Note that sampling becomes more uniform with increasing N. 10 | % - vis : optional logical input argument specifying if the point 11 | % samples should be visualized. vis=false is the default 12 | % setting. 13 | % 14 | % OUTPUT: 15 | % - V : N-by-3 array of vertex coordinates. 16 | % - Tri : M-by-3 list of face-vertex connectivities. 17 | % 18 | % 19 | % REFERENCES: 20 | % [1] Christopher Carlson, 'How I Made Wine Glasses from Sunflowers', 21 | % July 8, 2011. url: http://blog.wolfram.com/2011/07/28/how-i-made-wine-glasses-from-sunflowers/ 22 | % 23 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 24 | % 25 | 26 | 27 | if nargin<1 || isempty(N), N=200; end 28 | if nargin<2 || isempty(vis), vis=false; end 29 | 30 | N=round(N(1)); 31 | 32 | gr=(1+sqrt(5))/2; % golden ratio 33 | ga=2*pi*(1-1/gr); % golden angle 34 | 35 | i=0:(N-1); % particle (i.e., point sample) index 36 | lat=acos(1-2*i/(N-1)); % latitude is defined so that particle index is proportional to surface area between 0 and lat 37 | lon=i*ga; % position particles at even intervals along longitude 38 | 39 | % Convert from spherical to Cartesian coordinates 40 | x=sin(lat).*cos(lon); 41 | y=sin(lat).*sin(lon); 42 | z=cos(lat); 43 | V=[x(:) y(:) z(:)]; 44 | 45 | % Is triangulation required? 46 | Tri=[]; 47 | if nargout>1 48 | Tri=convhull(V); 49 | if ClosedMeshVolume({Tri V})<0, Tri=fliplr(Tri); end 50 | end 51 | 52 | % Visualize the result 53 | if ~vis, return; end 54 | tr=SubdivideSphericalMesh(IcosahedronMesh,5); 55 | figure('color','w') 56 | 57 | if ~isempty(Tri), ha1=subplot(1,2,1); end 58 | h=patch('faces',tr.ConnectivityList,'vertices',tr.Points); 59 | set(h,'EdgeColor','none','FaceColor',[0 0.8 0],'SpecularStrength',0.5) 60 | axis equal off vis3d 61 | hold on 62 | 63 | n=2; 64 | col=[1 0 0; 0 0 0]; 65 | for i=1:n 66 | plot3(x(i:n:N),y(i:n:N),z(i:n:N),'.r','MarkerSize',max(min(30*sqrt(1E3/N),30),5),'MarkerEdgeColor',col(i,:)) 67 | end 68 | 69 | if N<300 70 | r=1.001; 71 | i=linspace(0,N-1,1E5); 72 | lat=acos(1-2*i/(N-1)); 73 | lon=i*ga; 74 | x=sin(lat).*cos(lon); 75 | y=sin(lat).*sin(lon); 76 | z=cos(lat); 77 | plot3(r*x,r*y,r*z,'-k','Color',[0 0 0.4]) 78 | end 79 | 80 | light 81 | lighting phong 82 | view([20 30]) 83 | 84 | if isempty(Tri), return; end 85 | ha2=subplot(1,2,2); 86 | 87 | h=patch('faces',Tri,'vertices',V); 88 | set(h,'FaceColor','w','EdgeColor','k') 89 | axis equal off vis3d 90 | hold on 91 | for i=1:n 92 | plot3(V(i:n:N,1),V(i:n:N,2),V(i:n:N,3),'.r','MarkerSize',max(min(30*sqrt(1E3/N),30),5),'MarkerEdgeColor',col(i,:)) 93 | end 94 | set(ha2,'CameraViewAngle',get(ha1,'CameraViewAngle')) 95 | view([20 30]) 96 | 97 | -------------------------------------------------------------------------------- /QuadQuad.m: -------------------------------------------------------------------------------- 1 | function [fv,W,G]=QuadQuad(fv,W,G) 2 | % Subdivide a quadrilateral surface mesh using generalized quadrilateral 3 | % quadrisection. This procedure is similar to generalized triangular 4 | % quadrisection described in the documentation of 'TriQuad' function, the 5 | % only difference being is that it uses quad surface meshes as input. 6 | % 7 | % INPUT: 8 | % - fv : input mesh specified as a face-vertex structure with fields 9 | % 'vertices' and 'faces' so that fv.vertices contains a N-by-3 10 | % list of vertex co-ordinates and fv.faces contains a M-by-4 11 | % list of faces. Alternatively, fv can be specified as a cell so 12 | % that fv={F X}, where X is a N-by-3 list of vertex co-ordinates 13 | % and F is a M-by-4 list of faces. 14 | % - W : optional input argument. N-by-1 array of NON-ZERO, POSITIVE 15 | % vertex weights used during interpolation of the new vertices, 16 | % where N is the total number of the original mesh vertices. 17 | % - G : scalar or vector field defined on the mesh vertices (optional). 18 | % 19 | % OUTPUT: 20 | % - fv : subdivided mesh. 21 | % - W : new set of vertex weights. 22 | % 23 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 24 | % 25 | 26 | 27 | % Get the list of vertex co-ordinates and list of faces 28 | fmt=true; 29 | if isstruct(fv) && sum(isfield(fv,{'vertices' 'faces'}))==2 30 | F=fv.faces; 31 | X=fv.vertices; 32 | elseif iscell(fv) && numel(fv)==2 33 | F=fv{1}; 34 | X=fv{2}; 35 | fmt=false; 36 | else 37 | error('Unrecognized input format') 38 | end 39 | 40 | % Make sure that the mesh is composed entirely of quads 41 | if size(F,2)~=4 || ~isequal(round(F),F) 42 | error('Invalid entry for the list of faces') 43 | end 44 | 45 | % Check vertex weights 46 | if nargin<2 || isempty(W) 47 | W=[]; 48 | elseif ~ismatrix(W) || numel(W)~=size(X,1) || sum(W<=eps)>0 49 | error('W must be a N-by-1 array with positive entries, where N is the # of mesh vertices') 50 | else 51 | W=W(:); 52 | end 53 | 54 | % Field? 55 | if nargin==3 && ~isempty(G) && (size(G,1)~=size(X,1) || ~isnumeric(G) || ndims(G)>3) 56 | error('3-rd input argument must be a %u-by-d array where d>=1',size(X,1)) 57 | else 58 | G=[]; 59 | end 60 | 61 | % Edges 62 | E=[F(:,1) F(:,2); F(:,2) F(:,3); F(:,3) F(:,4); F(:,4) F(:,1)]; 63 | E=sort(E,2); 64 | [E,~,idx]=unique(E,'rows','stable'); % setOrder='stable' ensures that identical results will be obtained for meshes with the same connectivity 65 | 66 | % Compute new vertex positions 67 | if ~isempty(W) 68 | 69 | w=bsxfun(@rdivide,[W(E(:,1)),W(E(:,2))],W(E(:,1))+W(E(:,2))); 70 | w5=W(F); w5=bsxfun(@rdivide,w5,sum(w5,2)); 71 | 72 | V5=bsxfun(@times,w5(:,1),X(F(:,1),:)) + ... 73 | bsxfun(@times,w5(:,2),X(F(:,2),:)) + ... 74 | bsxfun(@times,w5(:,3),X(F(:,3),:)) + ... 75 | bsxfun(@times,w5(:,4),X(F(:,4),:)); 76 | 77 | V=cat(1,bsxfun(@times,X(E(:,1),:),w(:,1))+bsxfun(@times,X(E(:,2),:),w(:,2)),V5); 78 | 79 | if ~isempty(G) && nargout>2 80 | G5=bsxfun(@times,w5(:,1),G(F(:,1),:,:)) + ... 81 | bsxfun(@times,w5(:,2),G(F(:,2),:,:)) + ... 82 | bsxfun(@times,w5(:,3),G(F(:,3),:,:)) + ... 83 | bsxfun(@times,w5(:,4),G(F(:,4),:,:)); 84 | G=cat(1,G,bsxfun(@times,G(E(:,1),:,:),w(:,1))+bsxfun(@times,G(E(:,2),:,:),w(:,2)),G5); 85 | end 86 | 87 | else 88 | 89 | V5=(X(F(:,1),:)+X(F(:,2),:)+X(F(:,3),:)+X(F(:,4),:))/4; 90 | V=cat(1,(X(E(:,1),:)+X(E(:,2),:))/2,V5); 91 | 92 | if ~isempty(G) && nargout>2 93 | G5=(G(F(:,1),:,:)+G(F(:,2),:,:)+G(F(:,3),:,:)+G(F(:,4),:,:))/4; 94 | G=cat(1,G,(G(E(:,1),:,:)+G(E(:,2),:,:))/2,G5); 95 | end 96 | end 97 | 98 | % Assign indices to the new vertices 99 | Nx=size(X,1); % # of vertices 100 | Nt=size(F,1); % # of faces 101 | Ne=size(E,1); % # of edges 102 | 103 | V1= Nx + idx(1:Nt); 104 | V2= Nx + idx((Nt+1):2*Nt); 105 | V3= Nx + idx((2*Nt+1):3*Nt); 106 | V4= Nx + idx((3*Nt+1):4*Nt); 107 | V5= Nx + Ne + (1:Nt)'; 108 | 109 | % Connectivities of the new faces 110 | T1= [F(:,1) V1 V5 V4]; 111 | T2= [V1 F(:,2) V2 V5]; 112 | T3= [V5 V2 F(:,3) V3]; 113 | T4= [V4 V5 V3 F(:,4)]; 114 | 115 | T1=permute(T1,[3 1 2]); 116 | T2=permute(T2,[3 1 2]); 117 | T3=permute(T3,[3 1 2]); 118 | T4=permute(T4,[3 1 2]); 119 | 120 | F=cat(1,T1,T2,T3,T4); 121 | F=reshape(F,[],4,1); 122 | 123 | % New mesh 124 | if fmt 125 | clear fv 126 | fv.faces=F; 127 | fv.vertices=[X;V]; 128 | else 129 | fv={F [X;V]}; 130 | end 131 | 132 | -------------------------------------------------------------------------------- /TriQuad.m: -------------------------------------------------------------------------------- 1 | function [TR,W,G,SM,idx_unq]=TriQuad(TR,W,G) 2 | % Subdivide a triangular surface mesh using generalized triangular 3 | % quadrisection. Triangular quadrisection is a linear subdivision procedure 4 | % which inserts new vertices at the edge midpoints of the input mesh, 5 | % thereby producing four new faces for every face of the original mesh: 6 | % 7 | % x3 x3 8 | % / \ subdivision / \ 9 | % / \ ====> v3__v2 10 | % / \ / \ / \ 11 | % x1________x2 x1___v1___x2 12 | % 13 | % Original vertices : x1, x2, x3 14 | % 15 | % New vertices : v1, v2, v3 16 | % 17 | % New faces : [x1 v1 v3; x2 v2 v1; x3 v3 v2; v1 v2 v3] 18 | % 19 | % In case of generalized triangular quadrisection, positions of the newly 20 | % inserted vertices do not have to correspond to the edge midpoints, and 21 | % may be varied by assigning (positive) weights to the vertices of the 22 | % original mesh. For example, let xi and xj be two vertices connected by an 23 | % edge, and suppose that Wi and Wj are the corresponding vertex weights. 24 | % Position of the new point on the edge (xi,xj) is defined as (Wi*xi+Wj*xj)/(Wi+Wj). 25 | % Note that in order to avoid degeneracies and self-intersections, all 26 | % weights must be real numbers greater than zero. 27 | % 28 | % INPUT: 29 | % - TR : surface mesh represented as an object of 'TriRep' class, 30 | % 'triangulation' class, or a cell such that TR={Tri,X}, where 31 | % Tri is an M-by-3 array of faces and X is an N-by-3 array of 32 | % vertex coordinates. 33 | % - W : optional input argument specifying a N-by-1 array of STRICTLY 34 | % POSITIVE vertex weights used during interpolation of the new 35 | % vertices, where N is the total number of the original mesh 36 | % vertices. 37 | % - G : optional input specifying scalar or vector field defined at 38 | % the mesh vertices. 39 | % 40 | % OUTPUT: 41 | % - TR : subdivided mesh. Same format as the input mesh. 42 | % - W : interpolated vertex weights. 43 | % - G : interpolated scalar or vector field defined at the vertices of 44 | % the subdivided mesh. 45 | % - SM : subdivision matrix. 46 | % 47 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 48 | % 49 | 50 | 51 | % Get the list of vertex co-ordinates and list of faces 52 | [Tri,X,fmt]=GetMeshData(TR); 53 | 54 | % Make sure that the mesh is composed entirely of triangles 55 | if size(Tri,2)~=3 56 | error('This function is meant for triangular surface meshes, not quad or tet meshes') 57 | end 58 | 59 | % Check vertex weights 60 | if nargin<2 || isempty(W) 61 | W=[]; 62 | elseif ~ismatrix(W) || numel(W)~=size(X,1) || sum(W<=eps)>0 63 | error('W must be a N-by-1 array with positive entries, where N is the # of mesh vertices') 64 | else 65 | W=W(:); 66 | end 67 | 68 | % Field? 69 | if nargin==3 && ~isempty(G) && (size(G,1)~=size(X,1) || ~isnumeric(G) || ndims(G)>3) 70 | error('3-rd input argument must be a %u-by-d array where d>=1',size(X,1)) 71 | else 72 | G=[]; 73 | end 74 | 75 | 76 | % Edges 77 | E=[Tri(:,1) Tri(:,2); Tri(:,2) Tri(:,3); Tri(:,3) Tri(:,1)]; 78 | E=sort(E,2); 79 | [E,idx_unq,idx]=unique(E,'rows','stable'); % setOrder='stable' ensures that identical results will be obtained for meshes with the same connectivity 80 | 81 | % Compute new vertex positions 82 | if ~isempty(W) % insert new vertices based on vertex weights 83 | 84 | w=bsxfun(@rdivide,[W(E(:,1)),W(E(:,2))],W(E(:,1))+W(E(:,2))); 85 | V=bsxfun(@times,X(E(:,1),:),w(:,1))+bsxfun(@times,X(E(:,2),:),w(:,2)); 86 | 87 | if ~isempty(G) && nargout>2 88 | G=cat(1,G,bsxfun(@times,G(E(:,1),:,:),w(:,1))+bsxfun(@times,G(E(:,2),:,:),w(:,2))); 89 | end 90 | 91 | if nargout>1 92 | W=[W;W(E(:,1)).*w(:,1)+W(E(:,2)).*w(:,2)]; 93 | end 94 | 95 | else % insert new vertices at the edge mid-points 96 | 97 | V=(X(E(:,1),:)+X(E(:,2),:))/2; 98 | if ~isempty(G) && nargout>2 99 | G=cat(1,G,(G(E(:,1),:,:)+G(E(:,2),:,:))/2); 100 | end 101 | 102 | end 103 | 104 | % Generate a subdivision matrix if one is required 105 | Nx=size(X,1); % # of vertices 106 | Nt=size(Tri,1); % # of faces 107 | if nargout>3 108 | 109 | dNx=size(E,1); 110 | if isempty(W), w=repmat([1 1]/2,[dNx 1]); end 111 | 112 | i=(1:dNx)'+Nx; 113 | i=cat(1,(1:Nx)',i,i); 114 | j=cat(1,(1:Nx)',E(:)); 115 | w=cat(1,ones(Nx,1),w(:)); 116 | 117 | SM=sparse(i,j,w,Nx+dNx,Nx,2*dNx+Nx); 118 | 119 | end 120 | 121 | % Assign indices to new triangle vertices 122 | V1= Nx + idx(1:Nt); 123 | V2= Nx + idx((Nt+1):2*Nt); 124 | V3= Nx + idx((2*Nt+1):3*Nt); 125 | 126 | % Connectivities of the new faces 127 | T1= [Tri(:,1) V1 V3]; 128 | T2= [Tri(:,2) V2 V1]; 129 | T3= [Tri(:,3) V3 V2]; 130 | T4= [V1 V2 V3]; 131 | 132 | T1=permute(T1,[3 1 2]); 133 | T2=permute(T2,[3 1 2]); 134 | T3=permute(T3,[3 1 2]); 135 | T4=permute(T4,[3 1 2]); 136 | 137 | Tri=cat(1,T1,T2,T3,T4); 138 | Tri=reshape(Tri,[],3,1); 139 | 140 | % New mesh 141 | X=[X;V]; 142 | switch fmt 143 | case 1 144 | TR=triangulation(Tri,X); 145 | case 2 146 | TR=TriRep(Tri,X); %#ok<*DTRIREP> 147 | case 3 148 | TR={Tri X}; 149 | case 4 150 | TR=struct('faces',Tri,'vertices',X); 151 | end 152 | 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # S^2 Sampling Toolbox 2 | 3 | [![View Suite of functions to perform uniform sampling of a sphere on File Exchange](https://www.mathworks.com/matlabcentral/images/matlab-file-exchange.svg)](https://www.mathworks.com/matlabcentral/fileexchange/37004-suite-of-functions-to-perform-uniform-sampling-of-a-sphere) 4 | 5 | The problem of finding a uniform distribution of points on a sphere has a relatively long history. Its emergence is 6 | commonly attributed to the physicist J. J. Thomson, who posed it in 1904 after creating his so-called plum 7 | pudding model of the atom [[1]]. As such, the problem involves determination of a minimum energy configuration of N 8 | equally charged particles, confined to the surface of a sphere, that repel each other with a force given by Coulomb's 9 | law [[1]]. Although the plum pudding model of the atom has long been dismissed, the original problem posed by Thomson 10 | has re-emerged across many areas of study and found practical applications in the fields as diverse as viral 11 | morphology, crystallography, physical chemistry, geophysics, acoustics, signal processing, computer graphics, and 12 | medical imaging (e.g., HARDI). The purpose of this submission is to provide Matlab users with a set of functions for 13 | generating uniform sampling patterns and decompositions of a unit sphere. 14 | 15 | ![S2 sampling demo](https://user-images.githubusercontent.com/13392426/59448727-c5f08180-8dd3-11e9-950a-ba3eaee264e6.jpg) 16 | 17 | ## Summary of Main Functions 18 | 19 | >**`ParticleSampleSphere.m`**: generates an approximately uniform triangular tessellation of a unit sphere by using 20 | gradient descent to minimize a generalized electrostatic potential energy of a system of N charged particles. 21 | In this implementation, initial configuration of particles is based on random sampling of a sphere, but 22 | user-defined initializations are also permitted. This function can also be used to generate uniformly distributed 23 | sets of 2N particles comprised of N antipodal particle pairs. Since the optimization algorithm implemented in this 24 | function has O(N^2) complexity, it is not recommended that the function be used to optimize configurations of more 25 | than 1E3 particles (or particle pairs). Resolution of meshes obtained with this function can be increased to an 26 | arbitrary level with `SubdivideSphericalMesh.m`. 27 | 28 | >**`SubdivideSphericalMesh.m`**: increases resolution of triangular or quadrilateral spherical meshes. Given a base 29 | mesh, its resolution is increased by a sequence of k subdivisions. Suppose that No is the original number of 30 | mesh vertices, then the total number of vertices after k subdivisions will be Nk=4^k*(No – 2)+2. This relationship 31 | holds for both triangular and quadrilateral meshes. 32 | 33 | >**`IcosahedronMesh.m`**: generates triangular surface mesh of an icosahedron. High-quality spherical meshes can be 34 | easily obtained by subdividing this base mesh with the `SubdivideSphericalMesh.m` function. 35 | 36 | >**`QuadCubeMesh.m`**: generates quadrilateral mesh of a zero-centered unit cube. High-quality spherical meshes 37 | can be obtained by subdividing this base mesh with the `SubdivideSphericalMesh.m` function. 38 | 39 | >**`SpiralSampleSphere.m`**: generates N uniformly distributed point samples on a unit sphere using a [spiral-based sampling method]. 40 | 41 | >**`RandSampleSphere.m`**: performs uniform random or stratified random sampling of a unit sphere with N points. 42 | 43 | ## Demo 1: Uniformly Distributed Point Configurations on S^2 Via Charged Particle Sampling 44 | 45 | % Uniformly distribute 200 charged particles across unit sphere 46 | [V,Tri,~,Ue]=ParticleSampleSphere('N',200); 47 | 48 | % Visualize optimization progress 49 | figure('color','w') 50 | plot(log10(1:numel(Ue)),Ue,'.-') 51 | set(get(gca,'Title'),'String','Optimization Progress','FontSize',40) 52 | set(gca,'FontSize',20,'XColor','k','YColor','k') 53 | xlabel('log_{10}(Iteration #)','FontSize',30,'Color','k') 54 | ylabel('Reisz s-Energy','FontSize',30,'Color','k') 55 | 56 | % Visualize mesh based on computed configuration of particles 57 | figure('color','w') 58 | subplot(1,2,1) 59 | fv=struct('faces',Tri,'vertices',V); 60 | h=patch(fv); 61 | set(h,'EdgeColor','b','FaceColor','w') 62 | axis equal 63 | hold on 64 | plot3(V(:,1),V(:,2),V(:,3),'.k','MarkerSize',15) 65 | set(gca,'XLim',[-1.1 1.1],'YLim',[-1.1 1.1],'ZLim',[-1.1 1.1]) 66 | view(3) 67 | grid off 68 | set(get(gca,'Title'),'String','N=200 (base mesh)','FontSize',30) 69 | 70 | % Subdivide base mesh twice to obtain a spherical mesh of higher complexity 71 | fv_new=SubdivideSphericalMesh(fv,2); 72 | subplot(1,2,2) 73 | h=patch(fv_new); 74 | set(h,'EdgeColor','b','FaceColor','w') 75 | axis equal 76 | hold on 77 | plot3(V(:,1),V(:,2),V(:,3),'.k','MarkerSize',15) 78 | set(gca,'XLim',[-1.1 1.1],'YLim',[-1.1 1.1],'ZLim',[-1.1 1.1]) 79 | view(3) 80 | grid off 81 | set(get(gca,'Title'),'String','N=3170 (after 2 subdivisions)','FontSize',30) 82 | 83 | ## Demo 2: Uniform Antipodally Symmetric Point Configurations on S^2 Via Charged Particle Sampling 84 | 85 | % Uniformly distribute 100 antipodally symmetric particle pairs across unit sphere. Recall, 86 | % an antipodal partner of particle P is -P (i.e., P reflected through the origin). 87 | [V,Tri,~,Ue]=ParticleSampleSphere('N',100,'asym',true); 88 | 89 | % Visualize optimization progress 90 | figure('color','w') 91 | subplot(1,2,1) 92 | plot(log10(1:numel(Ue)),Ue,'.-') 93 | set(get(gca,'Title'),'String','Optimization Progress','FontSize',40) 94 | set(gca,'FontSize',20,'XColor','k','YColor','k') 95 | xlabel('log_{10}(Iteration #)','FontSize',30,'Color','k') 96 | ylabel('Reisz s-Energy','FontSize',30,'Color','k') 97 | 98 | % Visualize mesh based on computed configuration of particles. Note that unlike the previous 99 | % example, vertices of the mesh are (V;-V) and not (V). This is because -V are antipodal partners 100 | % of V and must be combined with V to uniformly sample the entire sphere. However, just like in 101 | % the previous example, computed mesh is also composed of 200 vertices, as there are 100 particle 102 | % pairs. 103 | subplot(1,2,2) 104 | fv=struct('faces',Tri,'vertices',[V;-V]); 105 | h=patch(fv); 106 | set(h,'EdgeColor','b','FaceColor','w') 107 | axis equal 108 | set(gca,'XLim',[-1.1 1.1],'YLim',[-1.1 1.1],'ZLim',[-1.1 1.1]) 109 | view(3) 110 | grid off 111 | hold on 112 | plot3(V(:,1),V(:,2),V(:,3),'.k','MarkerSize',15) 113 | plot3(-V(:,1),-V(:,2),-V(:,3),'.r','MarkerSize',15) 114 | set(get(gca,'Title'),'String','Final Mesh','FontSize',30) 115 | 116 | ## Demo 3: Spiral-based S^2 Sampling 117 | 118 | % Distribute 200 particles across unit sphere via spiral-based sampling method 119 | [V,Tri]=SpiralSampleSphere(200,true); 120 | 121 | ## Demo 4: Icosahedron-based S^2 Decomposition 122 | 123 | % Get base icosahedron mesh 124 | TR=IcosahedronMesh; % also try this with `DodecahedronMesh.m` 125 | 126 | % Subdivide base mesh and visualize the results 127 | figure('color','w') 128 | ha=subplot(2,3,1); 129 | h=trimesh(TR); set(h,'EdgeColor','b','FaceColor','w') 130 | axis equal 131 | set(get(ha,'Title'),'String','base mesh (N=12)') 132 | for i=2:6 133 | ha=subplot(2,3,i); 134 | TR=SubdivideSphericalMesh(TR,1); 135 | h=trimesh(TR); set(h,'EdgeColor','b','FaceColor','w') 136 | axis equal 137 | set(get(ha,'Title'),'String',sprintf('N=%u',4^(i-1)*10+2)) 138 | drawnow 139 | end 140 | 141 | ## Demo 5: Cuboid-based S^2 Decomposition 142 | 143 | % Get quad mesh of a unit cube 144 | fv=QuadCubeMesh; % also try this with `QuadRhombDodecMesh.m` 145 | 146 | % Subdivide base mesh and visualize the results 147 | figure('color','w') 148 | ha=subplot(2,3,1); 149 | h=patch(fv); set(h,'EdgeColor','b','FaceColor','w') 150 | view(3) 151 | grid on 152 | axis equal 153 | set(gca,'XLim',[-1.1 1.1],'YLim',[-1.1 1.1],'ZLim',[-1.1 1.1]) 154 | set(get(ha,'Title'),'String','base mesh (N=8)') 155 | for i=2:6 156 | ha=subplot(2,3,i); 157 | fv=SubdivideSphericalMesh(fv,1); 158 | h=patch(fv); set(h,'EdgeColor','b','FaceColor','w') 159 | axis equal 160 | view(3) 161 | grid on 162 | set(gca,'XLim',[-1.1 1.1],'YLim',[-1.1 1.1],'ZLim',[-1.1 1.1]) 163 | set(get(ha,'Title'),'String',sprintf('N=%u',4^(i-1)*6+2)) 164 | drawnow 165 | end 166 | 167 | ## License 168 | [MIT] © 2019 Anton Semechko (a.semechko@gmail.com) 169 | 170 | [1]: https://en.wikipedia.org/wiki/Thomson_problem 171 | [spiral-based sampling method]: http://blog.wolfram.com/2011/07/28/how-i-made-wine-glasses-from-sunflowers/ 172 | [MIT]: https://github.com/AntonSemechko/S2-Sampling-Toolbox/blob/master/LICENSE.md 173 | -------------------------------------------------------------------------------- /ParticleSampleSphere.m: -------------------------------------------------------------------------------- 1 | function [V,Tri,Ue_i,Ue]=ParticleSampleSphere(varargin) 2 | % Generate an approximately uniform triangular tessellation of a unit 3 | % sphere by minimizing generalized electrostatic potential energy 4 | % (i.e., Reisz s-energy) of a system of charged particles. Effectively, 5 | % this function produces a locally optimal solution to the problem that 6 | % involves finding a minimum Reisz s-energy configuration of N equally 7 | % charged particles confined to the surface of a unit sphere; s=1 8 | % corresponds to the problem originally posed by J.J. Thomson. 9 | % 10 | % SYNTAX: 11 | % [V,Tri,Ue_i,Ue]=ParticleSampleSphere(option_i,value_i,option_j,value_j,...) 12 | % 13 | % INPUT PARAMETERS/OPTIONS: 14 | % - 'N' : positive integer specifying either the desired number of 15 | % particles or antipodal particle pairs ( see 'asym' option 16 | % below). Default settings for 'N' is 200 when 'asym'=false 17 | % and 100 when 'asym'=true. If 'N' exceeds 1E3, user will be 18 | % prompted to manually verify if he wishes to continue. Set 19 | % 'qdlg' to false to disable manual verification when N>1E3. 20 | % The lowest permissible number of particles is 14. 21 | % - 'Vo' : array of particle positions used to initialize the search. 22 | % This input is optional and should be used in place of 'N' 23 | % when suboptimal initial configuration of particles is 24 | % available. Corresponding value of 'Vo' must be a N-by-3 25 | % array of particle coordinates, where N the number of 26 | % particles (or antipodal particle pairs if 'asym' is true). 27 | % Note that initializations consisting of more than 1E3 28 | % particles (or particle pairs) are admissible, but may lead 29 | % to unreasonably long optimization times. 30 | % - 's' : Reisz s-energy parameter used to control the strength of 31 | % particle interactions; higher values of 's' lead to stronger 32 | % short-range interactions . 's' must be a real number greater 33 | % than zero. 's'=1 is the default setting. 34 | % - 'asym' : compute antipodally symmetric particle configurations. Set 35 | % 'asym' to true to obtain a uniformly distributed set of 2N 36 | % particles comprised of N antipodal particle pairs. Recall, 37 | % an antipodal partner of particle P is -P (i.e., P reflected 38 | % through the origin). 'asym'=false is the default setting. 39 | % - 'Etol' : absolute energy convergence tolerance. Optimization will 40 | % terminate when change in potential energy between two 41 | % consecutive iterations falls below Etol. 'Etol'=1E-5 is the 42 | % default setting. 43 | % - 'Dtol' : maximum particle displacement tolerance (in degrees). 44 | % Optimization will terminate when maximum displacement of any 45 | % particle between two consecutive iteration is less than 46 | % Dtol. 'Dtol'=1E-4 is the default setting. 47 | % - 'Nitr' : maximum number of iterations. Nitr must be a non-negative 48 | % integer. 'Nitr'=1E4 is the default setting. 49 | % - 'upd' : progress update. Set 'upd' to false to disable progress 50 | % updates. 'upd'=true is the default setting. 51 | % - 'qdlg' : default maximum particle limit verification. Set 'qdlg' to 52 | % false to disable the question dialog pop-up prompting the 53 | % user to indicate if they wish to continue when N>1E3. 54 | % 'qdlg'=true is the default setting. 55 | % 56 | % REMAINS TO BE IMPLEMENTED 57 | % - 'CO' : connectivity optimization. To obtain particle configurations 58 | % with fewer dislocations (i.e., vertices possessing either 59 | % less or more than 6 neighbours) set 'CO' to true. 'CO'=false 60 | % is the default setting. 61 | % 62 | % OUTPUT: 63 | % - V : N-by-3 array of vertex (i.e., particle) coordinates. When 64 | % 'asym'=true, -V(i,:) is the antipodal partner of V(i,:). 65 | % - Tri : M-by-3 list of face-vertex connectivities. When 'asym'=false, 66 | % Tri is triangulation of V. When 'asym'=true, Tri is 67 | % triangulation of 2N particles [V;-V]. 68 | % - Ue_i : N-by-1 array of particle (or particle pair) energies. 69 | % - Ue : k-by-1 array of potential energy values, where k-1 is the 70 | % total number of iterations. Ue(1) and Ue(k) correspond to the 71 | % potential energies of the initial and final particle 72 | % configurations, respectively. 73 | % 74 | % 75 | % EXAMPLE 1: Uniformly distribute 200 particles across the surface of a 76 | % unit sphere 77 | % ------------------------------------------------------------------------- 78 | % % Sample 79 | % [V,Tri,~,Ue]=ParticleSampleSphere('N',200); 80 | % 81 | % % Visualize optimization progress 82 | % figure('color','w') 83 | % subplot(1,2,1) 84 | % plot(log10(1:numel(Ue)),Ue,'.-') 85 | % set(get(gca,'Title'),'String','Optimization Progress','FontSize',40) 86 | % set(gca,'FontSize',20,'XColor','k','YColor','k') 87 | % xlabel('log_{10}(Iteration #)','FontSize',30,'Color','k') 88 | % ylabel('Reisz s-Energy','FontSize',30,'Color','k') 89 | % 90 | % % Visualize mesh 91 | % subplot(1,2,2) 92 | % h=patch('faces',Tri,'vertices',V); 93 | % set(h,'EdgeColor','b','FaceColor','w') 94 | % axis equal 95 | % hold on 96 | % plot3(V(:,1),V(:,2),V(:,3),'.k','MarkerSize',15) 97 | % set(gca,'XLim',[-1.1 1.1],'YLim',[-1.1 1.1],'ZLim',[-1.1 1.1]) 98 | % view(3) 99 | % grid off 100 | % set(get(gca,'Title'),'String','N=200 (base mesh)','FontSize',30) 101 | % ------------------------------------------------------------------------- 102 | % 103 | % 104 | % EXAMPLE 2: Uniformly distribute 100 antipodally symmetric particle pairs 105 | % across the surface of a unit sphere 106 | % ------------------------------------------------------------------------- 107 | % %Sample 108 | %[V,Tri,~,Ue]=ParticleSampleSphere('N',100,'asym',true); 109 | 110 | % %Visualize optimization progress 111 | %figure('color','w') 112 | %subplot(1,2,1) 113 | %plot(log10(1:numel(Ue)),Ue,'.-') 114 | %set(get(gca,'Title'),'String','Optimization Progress','FontSize',40) 115 | %set(gca,'FontSize',20,'XColor','k','YColor','k') 116 | %xlabel('log_{10}(Iteration #)','FontSize',30,'Color','k') 117 | %ylabel('Reisz s-Energy','FontSize',30,'Color','k') 118 | 119 | % %Visualize mesh. Note that vertices of the mesh are [V;-V] and not [V] 120 | % %used in the previous example. This is because -V are antipodal partners 121 | % %of V. However, just like in the previous example, computed mesh is also 122 | % composed of 200 vertices. 123 | %subplot(1,2,2) 124 | %fv=struct('faces',Tri,'vertices',[V;-V]); 125 | %h=patch(fv); 126 | %set(h,'EdgeColor','b','FaceColor','w') 127 | %axis equal 128 | %set(gca,'XLim',[-1.1 1.1],'YLim',[-1.1 1.1],'ZLim',[-1.1 1.1]) 129 | %view(3) 130 | %grid off 131 | %hold on 132 | %plot3(V(:,1),V(:,2),V(:,3),'.k','MarkerSize',15) 133 | %plot3(-V(:,1),-V(:,2),-V(:,3),'.r','MarkerSize',15) 134 | %set(get(gca,'Title'),'String','Final Mesh','FontSize',30) 135 | % ------------------------------------------------------------------------- 136 | % 137 | % AUTHOR: Anton Semechko (a.semechko@gmail.com) 138 | % 139 | 140 | 141 | % Check the inputs 142 | prms=VerifyInputArgs(varargin); %#ok<*ASGLU> 143 | [V,s,upd,CO]=deal(prms.V,prms.s,prms.upd,prms.CO); 144 | 145 | if prms.Nitr<0 146 | [Tri,Ue_i,Ue]=deal([]); 147 | return 148 | end 149 | N=size(V,1); % number of particles 150 | prms.Dtol=prms.Dtol/180*pi; 151 | clear varargin 152 | 153 | if prms.Nitr>=0 || nargout>2 154 | 155 | % Compute geodesic distances between particle pairs 156 | DOT=max(min(V*V',1),-1); % dot product 157 | GD=acos(DOT); % geodesic distance 158 | 159 | % Evaluate potential energy 160 | GD(1:(N+1):end)=Inf; % set diagonal entries to Inf 161 | Ue_ij=1./(eps + GD.^s); 162 | if prms.asym 163 | Ue_ij=Ue_ij + 1./(eps + (pi-GD).^s); 164 | end 165 | Ue_i=sum(Ue_ij,2); 166 | if prms.asym 167 | Ue_i=Ue_i+(1/pi)^s; 168 | end 169 | Ue=sum(Ue_i); 170 | 171 | % Approximate average distance between two neighbouring particles 172 | if prms.asym 173 | d_ave=sqrt(8/sqrt(3)*pi/(2*N-2)); 174 | else 175 | d_ave=sqrt(8/sqrt(3)*pi/(N-2)); 176 | end 177 | if acos(1-d_ave^2/2)<1E-12 178 | fprintf(2,'Number of particles exceeds critical limit. Unable to continue due to limited numerical precision of ''acos'' function.\n') 179 | [Tri,Ue_i,Ue]=deal([]); 180 | return 181 | end 182 | d_thr=max(1E-3*d_ave,1E-14); 183 | 184 | end 185 | 186 | % Iteratively optimize particle positions along negative gradient of 187 | % potential energy using an adaptive Gauss-Seidel update scheme 188 | % ------------------------------------------------------------------------- 189 | if upd && prms.Nitr>0 190 | fprintf('\nWait while particle positions are being optimized ...\n') 191 | t0=clock; 192 | end 193 | 194 | a_min=1E-15; % minimum step size 195 | a_max=1; % maximum step size 196 | a=a_max*ones(N,1)/2; % step sizes used during position updates 197 | 198 | idx_jo=true(N,1); 199 | [dE,dV]=deal(Inf); 200 | Vrec=repmat({V},[1 10]); 201 | 202 | i=0; 203 | while iprms.Etol && dV>prms.Dtol 204 | 205 | i=i+1; 206 | 207 | % Sort particles according to their energy contribution 208 | [~,idx_sort]=sort(abs(Ue_i-mean(Ue_i)),'descend'); 209 | 210 | % Update positions of individual particles (or particle pairs) 211 | dV_max=0; 212 | for k=1:N 213 | 214 | j=idx_sort(k); 215 | 216 | idx_j=idx_jo; 217 | idx_j(j)=false; % particle indices, except the current one 218 | 219 | % Potential energy gradient of the j-th particle 220 | DOTj=DOT(idx_j,j); 221 | GDj=GD(idx_j,j); 222 | 223 | dVj=bsxfun(@times,s./(eps + sqrt(1-DOTj.^2)),V(idx_j,:)); 224 | if prms.asym 225 | dVj=bsxfun(@rdivide,dVj,eps + GDj.^(s+1)) - bsxfun(@rdivide,dVj,eps + (pi-GDj).^(s+1)); 226 | else 227 | dVj=bsxfun(@rdivide,dVj,eps + GDj.^(s+1)); 228 | end 229 | 230 | if min(GDj) 255 | end 256 | 257 | dV_max=2; 258 | break 259 | 260 | end 261 | 262 | end 263 | dVj=sum(dVj,1); 264 | 265 | % Only retain tangential component of the gradient 266 | dVj_n=(dVj*V(j,:)')*V(j,:); 267 | dVj_t=dVj-dVj_n; 268 | 269 | % Update position of the j-th particle 270 | m=0; 271 | Uj_old=sum(Ue_ij(j,:)); 272 | while m<50 273 | 274 | m=m+1; 275 | 276 | % Update position of the j-th particle 277 | Vj_new=V(j,:)-a(j)*dVj_t; 278 | Vj_new=Vj_new/norm(Vj_new); 279 | 280 | % Recompute dot products and geodesic distances 281 | DOTj=max(min(V*Vj_new(:),1),-1); 282 | GDj=acos(DOTj); 283 | GDj(j)=Inf; 284 | 285 | Ue_ij_j=1./(eps + GDj.^s); 286 | if prms.asym 287 | Ue_ij_j=Ue_ij_j + 1./(eps + (pi-GDj).^s); 288 | end 289 | 290 | % Check if the system potential decreased 291 | if sum(Ue_ij_j)a_min 312 | a(j)=max(a(j)/1.1,a_min); 313 | else 314 | break 315 | end 316 | 317 | end 318 | 319 | end 320 | 321 | end 322 | 323 | % Evaluate net potential energy of the system 324 | Ue_i=sum(Ue_ij,2); 325 | if prms.asym, Ue_i=Ue_i + (1/pi)^s; end 326 | Ue(i+1)=sum(Ue_i); 327 | 328 | % Maximum displacement (in radians) 329 | dV_max=acos(1-dV_max^2/2); 330 | Vrec{10}=V; 331 | Vrec=circshift(Vrec,[0 -1]); 332 | 333 | % Average change in potential energy 334 | if i>=10 335 | [dE,dE2]=deal((Ue(end-10)-Ue(end))/10); 336 | dV=dV_max; 337 | elseif i==1 338 | dE2=(Ue(1)-Ue(end))/i; 339 | end 340 | 341 | % Progress update 342 | if upd && ((mod(i,400)==0 && i>100) || i==1) 343 | fprintf('\n%-15s %-15s %-15s %-15s %-15s\n','Iteration #','log(Energy/N)','log(dE/Etol)','log(dV/Dtol)','Time (sec)') 344 | end 345 | 346 | if upd && (mod(i,10)==0 || i==1) 347 | fprintf('%-15u %-15.11f %-15.2f %-15.2f %-15.1f\n',i,log10(Ue(end)/size(V,1)),log10(max(dE2,eps)/prms.Etol),log10(max(dV_max,eps)/prms.Dtol),etime(clock,t0)) 348 | end 349 | 350 | % Reset step sizes; to avoid premature convergence 351 | if mod(i,40)==0 352 | a_max=min(a_max,5*max(a)); 353 | a(:)=a_max; 354 | end 355 | 356 | end 357 | clear DOT GD Ue_ij 358 | 359 | if upd && (mod(i,10)~=0 && i>1) 360 | fprintf('%-15u %-15.11f %-15.2f %-15.2f %-15.1f\n',i,log10(Ue(end)/size(V,1)),log10(dE/prms.Etol),log10(dV_max/prms.Dtol),etime(clock,t0)) 361 | end 362 | 363 | if upd && prms.Nitr>0 364 | fprintf('Optimization terminated after %u iterations. Elapsed time: %5.1f sec\n',i,etime(clock,t0)) 365 | fprintf('Convergence tolerances: Etol=%.6e, Dtol=%.6e degrees\n',prms.Etol,prms.Dtol/pi*180) 366 | end 367 | 368 | if ~prms.user_init 369 | if prms.asym 370 | idx=V(:,3)<0; 371 | V(idx,:)=-V(idx,:); 372 | end 373 | [~,id_srt]=sort(V(:,3),'descend'); 374 | V=V(id_srt,:); 375 | Ue_i=Ue_i(id_srt); 376 | end 377 | 378 | 379 | % Triangulate particle positions 380 | if nargout>1 381 | if prms.asym 382 | Tri=convhull([V;-V]); 383 | if ClosedMeshVolume({Tri [V;-V]})<0, Tri=fliplr(Tri); end 384 | else 385 | Tri=convhull(V); 386 | if ClosedMeshVolume({Tri V})<0, Tri=fliplr(Tri); end 387 | end 388 | end 389 | 390 | 391 | %========================================================================== 392 | function prms=VerifyInputArgs(VarsIn) 393 | % Make sure user-defined input arguments have valid format 394 | 395 | % Default settings 396 | prms.V=[]; 397 | prms.s=1; 398 | prms.Etol=1E-5; 399 | prms.Dtol=1E-4; 400 | prms.Nitr=1E4; 401 | prms.asym=false; 402 | prms.upd=true; 403 | prms.CO=false; 404 | prms.qdlg=true; 405 | prms.user_init=false; 406 | if isempty(VarsIn) 407 | prms.V=RandSampleSphere; 408 | return; 409 | end 410 | 411 | % Check that there is an even number of inputs 412 | Narg=numel(VarsIn); 413 | if mod(Narg,2)~=0 414 | error('This function only accepts name-value parameter pairs as inputs') 415 | end 416 | 417 | % Check user-defined parameters 418 | FNo={'N','Vo','s','asym','Etol','Dtol','Nitr','upd','CO','qdlg'}; 419 | flag=false(1,numel(FNo)); exit_flag=false; 420 | N=[]; 421 | for i=1:Narg/2 422 | 423 | % Make sure the input is a string 424 | str=VarsIn{2*(i-1)+1}; 425 | if ~ischar(str) || numel(str)>4 426 | error('Input argument #%u is not a valid paramer name',2*(i-1)+1) 427 | end 428 | 429 | % Get parameter "value" 430 | Val=VarsIn{2*i}; 431 | 432 | % Match the string against the list of available options 433 | chk=strcmpi(str,FNo); 434 | id=find(chk,1); 435 | if isempty(id), id=0; end 436 | 437 | switch id 438 | case 1 % number of particles (or particle pairs) 439 | 440 | % Check if 'initialization' option has also been specified 441 | if flag(2) 442 | error('Ambiguous combination of input parameters. Specify ''%s'' or ''%s'', but not both.',FNo{2},FNo{1}) 443 | end 444 | N=Val; 445 | 446 | case 2 % initialization 447 | 448 | % Check if 'number' option has also been specified 449 | if flag(1) 450 | error('Ambiguous combination of input parameters. Specify ''%s'' or ''%s'', but not both.',FNo{1},FNo{2}) 451 | end 452 | 453 | % Check the format 454 | if ~ismatrix(Val) || ~isnumeric(Val) || size(Val,2)~=3 || size(Val,1)<14 || any(~isfinite(Val(:))) 455 | error('Incorrect entry for ''%s''. ''%s'' must be set to a N-by-3 array, where N is the number of particles (or particle pairs).',FNo{2},FNo{2}) 456 | end 457 | 458 | % Make sure particles are on the unit sphere 459 | prms.V=ProjectOnSn(Val); 460 | prms.user_init=true; 461 | N=size(prms.V,1); 462 | 463 | case 3 % s parameter 464 | 465 | % Check the format 466 | if numel(Val)~=1 || ~isnumeric(Val) || ~isfinite(Val) || Val<1E-6 467 | error('Incorrect entry for the ''%s'' parameter. ''%s'' must be set to a positive real number.',FNo{3},FNo{3}) 468 | end 469 | prms.s=Val; 470 | 471 | case 4 % antipodal particle pairs 472 | 473 | % Check format 474 | if numel(Val)~=1 || ~islogical(Val) 475 | error('Incorrect entry for ''%s''. ''%s'' must be set to true or false.',FNo{4},FNo{4}) 476 | end 477 | prms.asym=Val; 478 | 479 | case 5 % energy tolerance 480 | 481 | % Check format 482 | if numel(Val)~=1 || ~isnumeric(Val) || ~isfinite(Val) || Val1E3 520 | 521 | % Check format 522 | if numel(Val)~=1 || ~islogical(Val) 523 | error('Incorrect entry for ''%s''. ''%s'' must be set to true or false.',FNo{10},FNo{10}) 524 | end 525 | prms.qdlg=Val; 526 | 527 | otherwise 528 | error('''%s'' is not a recognized parameter name',str) 529 | end 530 | flag(id)=true; 531 | 532 | end 533 | 534 | 535 | if ~isempty(N) 536 | chk=numel(N)~=1 | ~isnumeric(N) | any(~isfinite(N(:))) | ~isequal(round(N),N); 537 | if chk || (prms.asym && N<7) || (~prms.asym && N<14) 538 | error('Incorrect entry for ''N''. Total number of particles must be greater than 13.') 539 | end 540 | else 541 | N=200; 542 | end 543 | 544 | if isempty(prms.V) 545 | prms.V=RandSampleSphere(N); 546 | end 547 | 548 | % Check if there are more than 1E3 particles 549 | if N>1E3 && prms.qdlg 550 | 551 | % Construct a 'yes'/'no' questdlg 552 | choice = questdlg('Default particle limit exceeded. Would you like to continue?', ... 553 | 'Particle Limit Exceeded',' YES ',' NO ',' NO '); 554 | 555 | % Handle response 556 | if strcmpi(choice,' NO '), exit_flag=true; end 557 | 558 | end 559 | 560 | if exit_flag, prms.Nitr=-1; end %#ok<*UNRCH> 561 | 562 | --------------------------------------------------------------------------------