Discover MakerZone

MATLAB and Simulink resources for Arduino, LEGO, and Raspberry Pi

Learn more

Discover what MATLAB® can do for your career.

Opportunities for recent engineering grads.

Apply Today

Thread Subject:
Sparse grid quadrature

Subject: Sparse grid quadrature

From: Greg von Winckel

Date: 13 Dec, 2007 11:38:22

Message: 1 of 12

Thanks for your answers on my previous two threads. I have
noticed there is no sparse grid code on the FEX and would
like to submit my own, but before doing so, I would like to
post it here so that the experts might give their $0.02.


function [x,w]=spquad(dim,ord)

%SPQUAD
% Computes the sparse grid quadrature abscissae and weights
% on the [-1,1]^dim hypercube using the Clenshaw-Curtis rule.
%
% Example usage:
%
% f=@(x)
(1+x(:,1)).*exp(x(:,2).*x(:,3))+(1+x(:,2)).*exp(x(:,3).*x(:,1))
% [x,w]=spquad(3,4);
% Q=w'*f(x);

% node and weight for single point grid
x0=zeros(1,dim); w0=2^dim;

% Construct higher order grid hierarchically
for j=1:ord
    [x,w]=sparsegridnd(dim,j);
    [C,I]=intersect(x,x0,'rows');
    w(I)=w(I)+w0;
    x0=x; w0=w;
end


%% Multidimensional nodes and difference weights
function [x,w]=sparsegridnd(n,ord)

% Generates the n-dimensional sparse grid of order ord on the
% hypercube [-1,1]^n based on the Chebyshev-Gauss-Lobatto
points.

% Define 1D grid points
p=@(i) unique((i~=0)*cos(pi*2^(-i)*(0:(2^i)))');

% Get configurations of all possible subgrids
v=genindex(n,ord);

% Compute all possible 1D grids
P{1}=0; Q{1}=2;

% Compute all orders of one-dimensional quadrature rules

vmax=1+v(1,n);
for ii=2:vmax
    P{ii}=p(ii-1); Q{ii}=diffweight(ii-1);
end

% Take the union of all possible subgrids
% Need to find some way to preallocate the arrays
x=[]; w=[]; m=size(v,1);

for jj=1:m
    [tempx, tempw]=getpts(P,Q,v(jj,:));
    x=[x;tempx]; w=[w;tempw];
end

% Kludge to deal with small errors introduced by ndgrid
% need a better way to do this
roundn=@(a,n) round(a*10^n)/10^n;
x=roundn(x,15);

% Get unique points and weights
[x,ii,jj]=unique(x,'rows');
rows=length(ii); cols=length(jj);

% Form node condesation matix for combining weights
R=spalloc(rows,cols,cols);
for row=1:rows
    R(row,jj==row)=1;
end
w=R*w;


%% One dimensional difference weights
function dw=diffweight(ii)

if ii==1
    dw=[1;-2;1]/3;
else
    
% Define 1D quadrature weights
q=@(i) clencurt(2^(i)+1);

w=q(ii-1);
dw=q(ii);
dw(1:2:end)=dw(1:2:end)-w;

end


%% Compute the subset grid points and difference weights
function [X,W]=getpts(P,Q,v)

n=size(v,2);

[x{1:n}]=ndgrid(P{1+v});
[w{1:n}]=ndgrid(Q{1+v});

m=length(x{1}(:));
d=size(v,2);
W=ones(m,1);
X=zeros(m,d);

for k=1:d
   X(:,k)=x{k}(:);
   W=W.*w{k}(:);
end


%% Find all possible combinations of bases
function v=genindex(n, L1, head)

if n==1
    v = L1;
else
    v = cell2mat(arrayfun(@(j) genindex(n-1, L1-j, j), ...
                          (0:L1)', 'UniformOutput', false));
end

if nargin>=3
    v = [head+zeros(size(v,1),1) v];
end


%% Compute 1D Clenshaw-Curtis weights
function w=clencurt(N1)

if N1==1
     w=2;
else

N=N1-1; c=zeros(N1,1);
c(1:2:N1,1)=(2./[1 1-(2:2:N).^2 ])';
f=real(ifft([c(1:N1,:);c(N:-1:2,:)]));
w=2*([f(1,1); 2*f(2:N,1); f(N1,1)])/2;

end

Subject: Sparse grid quadrature

From: John D'Errico

Date: 13 Dec, 2007 13:52:31

Message: 2 of 12

"Greg von Winckel" <gregvw@gmail.com> wrote in message
<fjr5je$9k5$1@fred.mathworks.com>...
> Thanks for your answers on my previous two threads. I have
> noticed there is no sparse grid code on the FEX and would
> like to submit my own, but before doing so, I would like to
> post it here so that the experts might give their $0.02.
>
>
> function [x,w]=spquad(dim,ord)

Greg,

SPQUAD looks interesting for higher dimensional
quadrature. But perhaps I'm missing something
here. This test should work if I understand the
definition of n and ord in a quadrature context.

% a 5th order rule, for a 1-d problem.
[x,w]=spquad(1,5);

% p(x) = x^2 + x + 1
p = [1 1 1];

% the integral over [-1,1]
pint = diff(polyval([p./(length(p):-1:1),0],[-1 1]))
pint =
       2.6667

% Just in case my matlab was sloppy...
quad(@(x) polyval(p,x) ,-1, 1)
ans =
       2.6667

% test the spquad weights and nodes
f=@(x) polyval(p,x);
Q=w'*f(x)
Q =
       2.5595

I thought it would be exact for a 2nd order
polynomial. Have I misunderstood something
here?

John

Subject: Sparse grid quadrature

From: Greg von Winckel

Date: 13 Dec, 2007 15:07:48

Message: 3 of 12

John, it had not occurred to me to even check 1D as there is
no sparse grid per se. This is a valid point and I will add
a modification which simply does the full 1D grid for the
special case.

Best,

Greg

"John D'Errico" <woodchips@rochester.rr.com> wrote in
message <fjrdev$5r1$1@fred.mathworks.com>...
> "Greg von Winckel" <gregvw@gmail.com> wrote in message
> <fjr5je$9k5$1@fred.mathworks.com>...
> > Thanks for your answers on my previous two threads. I have
> > noticed there is no sparse grid code on the FEX and would
> > like to submit my own, but before doing so, I would like to
> > post it here so that the experts might give their $0.02.
> >
> >
> > function [x,w]=spquad(dim,ord)
>
> Greg,
>
> SPQUAD looks interesting for higher dimensional
> quadrature. But perhaps I'm missing something
> here. This test should work if I understand the
> definition of n and ord in a quadrature context.
>
> % a 5th order rule, for a 1-d problem.
> [x,w]=spquad(1,5);
>
> % p(x) = x^2 + x + 1
> p = [1 1 1];
>
> % the integral over [-1,1]
> pint = diff(polyval([p./(length(p):-1:1),0],[-1 1]))
> pint =
> 2.6667
>
> % Just in case my matlab was sloppy...
> quad(@(x) polyval(p,x) ,-1, 1)
> ans =
> 2.6667
>
> % test the spquad weights and nodes
> f=@(x) polyval(p,x);
> Q=w'*f(x)
> Q =
> 2.5595
>
> I thought it would be exact for a 2nd order
> polynomial. Have I misunderstood something
> here?
>
> John

Subject: Sparse grid quadrature

From: Greg von Winckel

Date: 13 Dec, 2007 15:13:32

Message: 4 of 12

1D tweak for completeness

function [x,w]=spquad(dim,ord)

%SPQUAD
% Computes the sparse grid quadrature abscissae and weights
% on the [-1,1]^dim hypercube using the Clenshaw-Curtis rule.
%
% Example usage:
%
% f=@(x)
(1+x(:,1)).*exp(x(:,2).*x(:,3))+(1+x(:,2)).*exp(x(:,3).*x(:,1))
% [x,w]=spquad(3,4);
% Q=w'*f(x);

% Special 1D case (not really a sparse grid)
if dim==1
   if ord==0
        x=0; w=2;
   else
        x=cos(pi*2^(-ord)*(0:(2^ord))');
        w=clencurt(2^(ord)+1);
   end
else
    
    
% node and weight for single point grid
x0=zeros(1,dim); w0=2^dim;

% Construct higher order grid hierarchically
for j=1:ord
    [x,w]=sparsegridnd(dim,j);
    [C,I]=intersect(x,x0,'rows');
    w(I)=w(I)+w0;
    x0=x; w0=w;
end

end


%% Multidimensional nodes and difference weights
function [x,w]=sparsegridnd(n,ord)

% Generates the n-dimensional sparse grid of order ord on the
% hypercube [-1,1]^n based on the Chebyshev-Gauss-Lobatto
points.

% Define 1D grid points
p=@(i) unique((i~=0)*cos(pi*2^(-i)*(0:(2^i)))');

% Get configurations of all possible subgrids
v=genindex(n,ord);

% Compute all possible 1D grids
P{1}=0; Q{1}=2;

% Compute all orders of one-dimensional quadrature rules

vmax=1+v(1,n);
for ii=2:vmax
    P{ii}=p(ii-1); Q{ii}=diffweight(ii-1);
end

% Take the union of all possible subgrids
% Need to find some way to preallocate the arrays
x=[]; w=[]; m=size(v,1);

for jj=1:m
    [tempx, tempw]=getpts(P,Q,v(jj,:));
    x=[x;tempx]; w=[w;tempw];
end

% Kludge to deal with small errors introduced by ndgrid
% need a better way to do this
roundn=@(a,n) round(a*10^n)/10^n;
x=roundn(x,15);

% Get unique points and weights
[x,ii,jj]=unique(x,'rows');
rows=length(ii); cols=length(jj);

% Form node condesation matix for combining weights
R=spalloc(rows,cols,cols);
for row=1:rows
    R(row,jj==row)=1;
end
w=R*w;


%% One dimensional difference weights
function dw=diffweight(ii)

if ii==1
    dw=[1;-2;1]/3;
else
    
% Define 1D quadrature weights
q=@(i) clencurt(2^(i)+1);

w=q(ii-1);
dw=q(ii);
dw(1:2:end)=dw(1:2:end)-w;

end


%% Compute the subset grid points and difference weights
function [X,W]=getpts(P,Q,v)

n=size(v,2);

[x{1:n}]=ndgrid(P{1+v});
[w{1:n}]=ndgrid(Q{1+v});

m=length(x{1}(:));
d=size(v,2);
W=ones(m,1);
X=zeros(m,d);

for k=1:d
   X(:,k)=x{k}(:);
   W=W.*w{k}(:);
end


%% Find all possible combinations of bases
function v=genindex(n, L1, head)

if n==1
    v = L1;
else
    v = cell2mat(arrayfun(@(j) genindex(n-1, L1-j, j), ...
                          (0:L1)', 'UniformOutput', false));
end

if nargin>=3
    v = [head+zeros(size(v,1),1) v];
end


%% Compute 1D Clenshaw-Curtis weights
function w=clencurt(N1)

if N1==1
     w=2;
else

N=N1-1; c=zeros(N1,1);
c(1:2:N1,1)=(2./[1 1-(2:2:N).^2 ])';
f=real(ifft([c(1:N1,:);c(N:-1:2,:)]));
w=2*([f(1,1); 2*f(2:N,1); f(N1,1)])/2;

end

Subject: Sparse grid quadrature

From: John D'Errico

Date: 13 Dec, 2007 16:05:23

Message: 5 of 12

"Greg von Winckel" <gregvw@gmail.com> wrote in message
<fjri6s$65g$1@fred.mathworks.com>...
> 1D tweak for completeness

Better now. I need to play with it some more and
read through the code.

John

Subject: Sparse grid quadrature

From: Greg von Winckel

Date: 13 Dec, 2007 16:28:59

Message: 6 of 12

Thanks John. Please let me know if you have any questions or
I have not made something clear.

Best,

Greg

"John D'Errico" <woodchips@rochester.rr.com> wrote in
message <fjrl83$19o$1@fred.mathworks.com>...
> "Greg von Winckel" <gregvw@gmail.com> wrote in message
> <fjri6s$65g$1@fred.mathworks.com>...
> > 1D tweak for completeness
>
> Better now. I need to play with it some more and
> read through the code.
>
> John

Subject: Sparse grid quadrature

From: John D'Errico

Date: 13 Dec, 2007 22:34:26

Message: 7 of 12

"Greg von Winckel" <gregvw@gmail.com> wrote in message
<fjrmkb$r2j$1@fred.mathworks.com>...
> Thanks John. Please let me know if you have any questions or
> I have not made something clear.

Well, you did ask for it. As a start, I like this.
I really do. In fact, I think this could be a
solution for many people who wish to use
higher dimensional numerical quadrature.
But I'd suggest that

1. You should provide an interface that allows
user to call it several ways.

% current interface
[x,w] = spquad(n,ord);

% allow a more general hyper-rectangle
[x,w] = spquad(n,ord,intervals);

% provide a function to evaluate
Q = spquad(fun,n,ord);

% a function over some general hyper-rect
Q = spquad(fun,n,ord,intervals);

Here fun is either an anonymous function,
an inline function, or a character string that
denotes the name of a function m-file.

Intervals must be an nx2 numeric array if
provided, defining the lower and upper
bounds in each dimension of the hyper
rectangle. As a default, intervals would
be the array repmat([-1,1],n,1).

All you need to do is to test if the first
argument is a numeric scalar, or anything
else.

2. Provide a reference on Clenshaw-Curtis,
and on the specifics of this tool, if you have
a reference. If not, then a pdf file would be
useful.

3. Add some comments in the help explaining
that this is not an adaptive quadrature tool
like quad.

I'll get into the code itself next.

John

Subject: Sparse grid quadrature

From: Greg von Winckel

Date: 14 Dec, 2007 07:58:32

Message: 8 of 12

Thanks for your suggestions, John. I will look into
implementing them. Regarding the code itself, I am trying to
figure out how to preallocate the quantities which mlint
gripes about. i.e. what is the appropriate size.

The other issue is how to better deal with the node
condensation without using rounding at the 15th decimal place.

Thanks again and I will post progress as it happens.

Best,

Greg

Subject: Sparse grid quadrature

From: Greg von Winckel

Date: 14 Dec, 2007 15:39:17

Message: 9 of 12

I think this revision should address the generalization to a
hyperrectangular domain

function [x,w]=spquad(dim,ord,bpt)

%SPQUAD
% Computes the sparse grid quadrature abscissae and weights
% on an orthotope/hyperrectangle using the Clenshaw-Curtis rule.
%
% [X,W]=SPQUAD(DIM,ORD,AB)
%
% Input Parameters:
% DIM - number of dimensions
% ORD - order of the integration rule
% BPT - boundary points
%
% If used, BPT should be a 2 by DIM matrix containing the
% endpoints of the hyperrectangle in each column.
%
% Example usage:
%
% f=@(x)
(1+x(:,1)).*exp(x(:,2).*x(:,3))+(1+x(:,2)).*exp(x(:,3).*x(:,1))
% [x,w]=spquad(3,4,[-1 0 2; 1 1 3]);
% Q=w'*f(x);

if nargin>2
    if size(bpt,2)~=dim
        error('dimension mismatch');
    end
else
    bpt=repmat([-1,1]',1,dim);
end

% Length and midpoint in each dimesion
len=diff(bpt); mpt=mean(bpt);

if ord==0 % zeroth order case is just the midpoint rule
    x=mpt;
    w=prod(len);
elseif ord~=0 && dim==1 % Special 1D case
    x=mpt+len*cos(pi*2^(-ord)*(0:(2^ord))')/2;
    w=clencurt(2^(ord)+1)*len/2;
else
    
    
% node and weight for single point grid
x0=mpt; w0=prod(len);

% Construct higher order grid hierarchically
for j=1:ord
    [x,w]=sparsegridnd(dim,j,mpt,len);
    [C,I]=intersect(x,x0,'rows');
    w(I)=w(I)+w0;
    x0=x; w0=w;
end

end


%% Multidimensional nodes and difference weights
function [x,w]=sparsegridnd(n,ord,mpt,len)

% Generates the n-dimensional sparse grid of order ord on the
% hypercube [-1,1]^n based on the Chebyshev-Gauss-Lobatto
points.

% Define 1D grid points
p=@(i) unique((i~=0)*cos(pi*2^(-i)*(0:(2^i)))');

% Get configurations of all possible subgrids
v=genindex(n,ord);

% Compute all possible 1D grids
P{1}=0; Q{1}=2;

% Compute all orders of one-dimensional quadrature rules

vmax=1+v(1,n);
for ii=2:vmax
    P{ii}=p(ii-1); Q{ii}=diffweight(ii-1);
end

% Take the union of all possible subgrids
% Need to find some way to preallocate the arrays
x=[]; w=[]; m=size(v,1);

for jj=1:m
    [tempx, tempw]=getpts(P,Q,v(jj,:),mpt,len);
    x=[x;tempx]; w=[w;tempw];
end

% Kludge to deal with small errors introduced by ndgrid
% need a better way to do this
roundn=@(a,n) round(a*10^n)/10^n;
x=roundn(x,15);

% Get unique points and weights
[x,ii,jj]=unique(x,'rows');
rows=length(ii); cols=length(jj);

% Form node condesation matix for combining weights
R=spalloc(rows,cols,cols);
for row=1:rows
    R(row,jj==row)=1;
end
w=R*w;


%% One dimensional difference weights
function dw=diffweight(ii)

if ii==1
    dw=[1;-2;1]/3;
else
    
% Define 1D quadrature weights
q=@(i) clencurt(2^(i)+1);

w=q(ii-1);
dw=q(ii);
dw(1:2:end)=dw(1:2:end)-w;

end


%% Compute the subset grid points and difference weights
function [X,W]=getpts(P,Q,v,mpt,len)

n=size(v,2);

[x{1:n}]=ndgrid(P{1+v});
[w{1:n}]=ndgrid(Q{1+v});

m=length(x{1}(:));
d=size(v,2);
W=ones(m,1);
X=zeros(m,d);

for k=1:d
   X(:,k)=mpt(k)+x{k}(:)*len(k)/2;
   W=W.*w{k}(:)*len(k)/2;
end


%% Find all possible combinations of bases
function v=genindex(n, L1, head)

if n==1
    v = L1;
else
    v = cell2mat(arrayfun(@(j) genindex(n-1, L1-j, j), ...
                          (0:L1)', 'UniformOutput', false));
end

if nargin>=3
    v = [head+zeros(size(v,1),1) v];
end


%% Compute 1D Clenshaw-Curtis weights
function w=clencurt(N1)

if N1==1
     w=2;
else

N=N1-1; c=zeros(N1,1);
c(1:2:N1,1)=(2./[1 1-(2:2:N).^2 ])';
f=real(ifft([c(1:N1,:);c(N:-1:2,:)]));
w=2*([f(1,1); 2*f(2:N,1); f(N1,1)])/2;

end

Subject: Sparse grid quadrature

From: Tim Davis

Date: 14 Dec, 2007 18:31:18

Message: 10 of 12

"Greg von Winckel" <gregvw@gmail.com> wrote in message

<fjr5je$9k5$1@fred.mathworks.com>...
> Thanks for your answers on my previous two threads. I have
> noticed there is no sparse grid code on the FEX and would
> like to submit my own, but before doing so, I would like to
> post it here so that the experts might give their $0.02.

...
> % Form node condesation matix for combining weights
> R=spalloc(rows,cols,cols);
> for row=1:rows
> R(row,jj==row)=1;
> end
> w=R*w;
>

My $0.02: the above code will be outrageously slow. See
Loren's March 1st, 2007, blog. R should be created with
R=sparse(I,J,X,N,N), not with spalloc followed by sparse
R(i,j)=whatever

Subject: Sparse grid quadrature

From: Greg von Winckel

Date: 17 Dec, 2007 11:54:15

Message: 11 of 12

> My $0.02: the above code will be outrageously slow. See
> Loren's March 1st, 2007, blog. R should be created with
> R=sparse(I,J,X,N,N), not with spalloc followed by sparse
> R(i,j)=whatever

It turns out since that only matrix multiplication is
needed, this matrix can be replaced by a function which
performs its action.

function [x,w]=spquad(dim,ord,bpt)

%SPQUAD
% Computes the sparse grid quadrature abscissae and weights
% on an orthotope/hyperrectangle using the Clenshaw-Curtis rule.
%
% [X,W]=SPQUAD(DIM,ORD,AB)
%
% Input Parameters:
% DIM - number of dimensions
% ORD - order of the integration rule
% BPT - boundary points
%
% If used, BPT should be a 2 by DIM matrix containing the
% endpoints of the hyperrectangle in each column.
%
% Example usage:
%
% f=@(x)
(1+x(:,1)).*exp(x(:,2).*x(:,3))+(1+x(:,2)).*exp(x(:,3).*x(:,1))
% [x,w]=spquad(3,4,[-1 0 2; 1 1 3]);
% Q=w'*f(x);

if nargin>2
    if size(bpt,2)~=dim
        error('dimension mismatch');
    end
else
    bpt=repmat([-1,1]',1,dim);
end

% Length and midpoint in each dimesion
len=diff(bpt); mpt=mean(bpt);

if ord==0 % zeroth order case is just the midpoint rule
    x=mpt; w=prod(len);
elseif ord~=0 && dim==1 % Special 1D case
    x=mpt+len*cos(pi*2^(-ord)*(0:(2^ord))')/2;
    w=clencurt(2^(ord)+1)*len/2;
else
        
    % node and weight for single point grid
    x0=mpt; w0=prod(len);

    % Construct higher order grid hierarchically
    for j=1:ord
        [x,w]=sparsegridnd(dim,j,mpt,len);
        [C,I]=intersect(x,x0,'rows');
        w(I)=w(I)+w0; x0=x; w0=w;
    end

end


%% Multidimensional nodes and difference weights
function [x,w]=sparsegridnd(n,ord,mpt,len)

% Generates the n-dimensional sparse grid of order ord on the
% hypercube [-1,1]^n based on the Chebyshev-Gauss-Lobatto
points.

% Define 1D grid points
p=@(i) unique((i~=0)*cos(pi*2^(-i)*(0:(2^i)))');

% Get configurations of all possible subgrids
v=genindex(n,ord); vmax=1+v(1,n);

% Compute all orders of one-dimensional quadrature rules
P=arrayfun(@(j) p(j), (0:vmax-1),'UniformOutPut',0);
Q=arrayfun(@(j) diffweight(j), (0:vmax),'UniformOutPut',0);

% Take the union of all possible subgrids
% Need to find some way to preallocate the arrays
m=size(v,1);

xw=cell2mat(arrayfun(@(k) getpts(P,Q,v(k,:),mpt,len), (1:m)',...
    'UniformOutPut',false));
x=xw(:,1:n); w=xw(:,n+1);

% Kludge to deal with small errors introduced by ndgrid
% need a better way to do this
roundn=@(a,n) round(a*10^n)/10^n;
x=roundn(x,15);

% Get unique points and weights
[x,ii,jj]=unique(x,'rows');
rows=length(ii);

% Do node condesation for combining weights
w=cell2mat(arrayfun(@(j) (jj==j)'*w,
(1:rows)','UniformOutPut',0));


%% One dimensional difference weights
function dw=diffweight(ii)

if ii==0
    dw=2;
elseif ii==1
    dw=[1;-2;1]/3;
else
    
    % Define 1D quadrature weights
    q=@(i) clencurt(2^(i)+1);
    dw=q(ii); dw(1:2:end)=dw(1:2:end)-q(ii-1);

end


%% Compute the subset grid points and difference weights
function [XW]=getpts(P,Q,v,mpt,len)

n=size(v,2);

[x{1:n}]=ndgrid(P{1+v});
[w{1:n}]=ndgrid(Q{1+v});

m=length(x{1}(:)); d=size(v,2);
W=ones(m,1); X=zeros(m,d);

for k=1:d
   X(:,k)=mpt(k)+x{k}(:)*len(k)/2;
   W=W.*w{k}(:);
end

XW=[X,W];


%% Find all possible combinations of bases
function v=genindex(n, L1, head)

if n==1
    v = L1;
else
    v = cell2mat(arrayfun(@(j) genindex(n-1, L1-j, j), ...
                          (0:L1)', 'UniformOutput', 0));
end

if nargin>=3
    v = [head+zeros(size(v,1),1) v];
end


%% Compute 1D Clenshaw-Curtis weights
function w=clencurt(N1)

if N1==1
     w=2;
else
    N=N1-1; c=zeros(N1,1);
    c(1:2:N1,1)=(2./[1 1-(2:2:N).^2 ])';
    f=real(ifft([c(1:N1,:);c(N:-1:2,:)]));
    w=2*([f(1,1); 2*f(2:N,1); f(N1,1)])/2;
end

Subject: Sparse grid quadrature

From: Sena

Date: 19 Dec, 2007 20:29:24

Message: 12 of 12

Nice work Greg. Can you recode SPQUAD so that variable
accuracy levels can be applied in different dimensions?

Tags for this Thread

What are tags?

A tag is like a keyword or category label associated with each thread. Tags make it easier for you to find threads of interest.

Anyone can tag a thread. Tags are public and visible to everyone.

Contact us