Author: luc
Date: Sun Jan  4 10:08:50 2009
New Revision: 731308

URL: http://svn.apache.org/viewvc?rev=731308&view=rev
Log:
simplified tests
added new tests

Modified:
    
commons/proper/math/trunk/src/test/org/apache/commons/math/linear/QRDecompositionImplTest.java
    
commons/proper/math/trunk/src/test/org/apache/commons/math/linear/QRSolverTest.java

Modified: 
commons/proper/math/trunk/src/test/org/apache/commons/math/linear/QRDecompositionImplTest.java
URL: 
http://svn.apache.org/viewvc/commons/proper/math/trunk/src/test/org/apache/commons/math/linear/QRDecompositionImplTest.java?rev=731308&r1=731307&r2=731308&view=diff
==============================================================================
--- 
commons/proper/math/trunk/src/test/org/apache/commons/math/linear/QRDecompositionImplTest.java
 (original)
+++ 
commons/proper/math/trunk/src/test/org/apache/commons/math/linear/QRDecompositionImplTest.java
 Sun Jan  4 10:08:50 2009
@@ -17,6 +17,8 @@
 
 package org.apache.commons.math.linear;
 
+import java.util.Random;
+
 import junit.framework.Test;
 import junit.framework.TestCase;
 import junit.framework.TestSuite;
@@ -59,143 +61,152 @@
 
     /** test dimensions */
     public void testDimensions() {
-        RealMatrix matrix = 
MatrixUtils.createRealMatrix(testData3x3NonSingular);
-        QRDecomposition qr = new QRDecompositionImpl(matrix);
-        assertEquals("3x3 Q size", qr.getQ().getRowDimension(), 3);
-        assertEquals("3x3 Q size", qr.getQ().getColumnDimension(), 3);
-        assertEquals("3x3 R size", qr.getR().getRowDimension(), 3);
-        assertEquals("3x3 R size", qr.getR().getColumnDimension(), 3);
+        checkDimension(MatrixUtils.createRealMatrix(testData3x3NonSingular));
 
-        matrix = MatrixUtils.createRealMatrix(testData4x3);
-        qr = new QRDecompositionImpl(matrix);
-        assertEquals("4x3 Q size", qr.getQ().getRowDimension(), 4);
-        assertEquals("4x3 Q size", qr.getQ().getColumnDimension(), 4);
-        assertEquals("4x3 R size", qr.getR().getRowDimension(), 4);
-        assertEquals("4x3 R size", qr.getR().getColumnDimension(), 3);
+        checkDimension(MatrixUtils.createRealMatrix(testData4x3));
 
-        matrix = MatrixUtils.createRealMatrix(testData3x4);
-        qr = new QRDecompositionImpl(matrix);
-        assertEquals("3x4 Q size", qr.getQ().getRowDimension(), 3);
-        assertEquals("3x4 Q size", qr.getQ().getColumnDimension(), 3);
-        assertEquals("3x4 R size", qr.getR().getRowDimension(), 3);
-        assertEquals("3x4 R size", qr.getR().getColumnDimension(), 4);
+        checkDimension(MatrixUtils.createRealMatrix(testData3x4));
+
+        Random r = new Random(643895747384642l);
+        int    p = (5 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        int    q = (7 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        checkDimension(createTestMatrix(r, p, q));
+        checkDimension(createTestMatrix(r, q, p));
+
+    }
+
+    private void checkDimension(RealMatrix m) {
+        int rows = m.getRowDimension();
+        int columns = m.getColumnDimension();
+        QRDecomposition qr = new QRDecompositionImpl(m);
+        assertEquals(rows,    qr.getQ().getRowDimension());
+        assertEquals(rows,    qr.getQ().getColumnDimension());
+        assertEquals(rows,    qr.getR().getRowDimension());
+        assertEquals(columns, qr.getR().getColumnDimension());        
     }
 
     /** test A = QR */
     public void testAEqualQR() {
-        RealMatrix A = MatrixUtils.createRealMatrix(testData3x3NonSingular);
-        QRDecomposition qr = new QRDecompositionImpl(A);
-        RealMatrix Q = qr.getQ();
-        RealMatrix R = qr.getR();
-        double norm = Q.multiply(R).subtract(A).getNorm();
-        assertEquals("3x3 nonsingular A = QR", 0, norm, normTolerance);
-
-        RealMatrix matrix = MatrixUtils.createRealMatrix(testData3x3Singular);
-        qr = new QRDecompositionImpl(matrix);
-        norm = qr.getQ().multiply(qr.getR()).subtract(matrix).getNorm();
-        assertEquals("3x3 singular A = QR", 0, norm, normTolerance);
+        checkAEqualQR(MatrixUtils.createRealMatrix(testData3x3NonSingular));
 
-        matrix = MatrixUtils.createRealMatrix(testData3x4);
-        qr = new QRDecompositionImpl(matrix);
-        norm = qr.getQ().multiply(qr.getR()).subtract(matrix).getNorm();
-        assertEquals("3x4 A = QR", 0, norm, normTolerance);
+        checkAEqualQR(MatrixUtils.createRealMatrix(testData3x3Singular));
+
+        checkAEqualQR(MatrixUtils.createRealMatrix(testData3x4));
+
+        checkAEqualQR(MatrixUtils.createRealMatrix(testData4x3));
+
+        Random r = new Random(643895747384642l);
+        int    p = (5 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        int    q = (7 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        checkAEqualQR(createTestMatrix(r, p, q));
+
+        checkAEqualQR(createTestMatrix(r, q, p));
 
-        matrix = MatrixUtils.createRealMatrix(testData4x3);
-        qr = new QRDecompositionImpl(matrix);
-        norm = qr.getQ().multiply(qr.getR()).subtract(matrix).getNorm();
-        assertEquals("4x3 A = QR", 0, norm, normTolerance);
+    }
+
+    private void checkAEqualQR(RealMatrix m) {
+        QRDecomposition qr = new QRDecompositionImpl(m);
+        double norm = qr.getQ().multiply(qr.getR()).subtract(m).getNorm();
+        assertEquals(0, norm, normTolerance);
     }
 
     /** test the orthogonality of Q */
     public void testQOrthogonal() {
-        RealMatrix matrix = 
MatrixUtils.createRealMatrix(testData3x3NonSingular);
-        RealMatrix q  = new QRDecompositionImpl(matrix).getQ();
-        RealMatrix qT = new QRDecompositionImpl(matrix).getQT();
-        RealMatrix eye = MatrixUtils.createRealIdentityMatrix(3);
-        double norm = qT.multiply(q).subtract(eye).getNorm();
-        assertEquals("3x3 nonsingular Q'Q = I", 0, norm, normTolerance);
+        checkQOrthogonal(MatrixUtils.createRealMatrix(testData3x3NonSingular));
 
-        matrix = MatrixUtils.createRealMatrix(testData3x3Singular);
-        q  = new QRDecompositionImpl(matrix).getQ();
-        qT = new QRDecompositionImpl(matrix).getQT();
-        eye = MatrixUtils.createRealIdentityMatrix(3);
-        norm = qT.multiply(q).subtract(eye).getNorm();
-        assertEquals("3x3 singular Q'Q = I", 0, norm, normTolerance);
+        checkQOrthogonal(MatrixUtils.createRealMatrix(testData3x3Singular));
 
-        matrix = MatrixUtils.createRealMatrix(testData3x4);
-        q  = new QRDecompositionImpl(matrix).getQ();
-        qT = new QRDecompositionImpl(matrix).getQT();
-        eye = MatrixUtils.createRealIdentityMatrix(3);
-        norm = qT.multiply(q).subtract(eye).getNorm();
-        assertEquals("3x4 Q'Q = I", 0, norm, normTolerance);
+        checkQOrthogonal(MatrixUtils.createRealMatrix(testData3x4));
+
+        checkQOrthogonal(MatrixUtils.createRealMatrix(testData4x3));
+
+        Random r = new Random(643895747384642l);
+        int    p = (5 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        int    q = (7 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        checkQOrthogonal(createTestMatrix(r, p, q));
+
+        checkQOrthogonal(createTestMatrix(r, q, p));
 
-        matrix = MatrixUtils.createRealMatrix(testData4x3);
-        q  = new QRDecompositionImpl(matrix).getQ();
-        qT = new QRDecompositionImpl(matrix).getQT();
-        eye = MatrixUtils.createRealIdentityMatrix(4);
-        norm = qT.multiply(q).subtract(eye).getNorm();
-        assertEquals("4x3 Q'Q = I", 0, norm, normTolerance);
+    }
+
+    private void checkQOrthogonal(RealMatrix m) {
+        QRDecomposition qr = new QRDecompositionImpl(m);
+        RealMatrix eye = 
MatrixUtils.createRealIdentityMatrix(m.getRowDimension());
+        double norm = qr.getQT().multiply(qr.getQ()).subtract(eye).getNorm();
+        assertEquals(0, norm, normTolerance);
     }
 
     /** test that R is upper triangular */
     public void testRUpperTriangular() {
         RealMatrix matrix = 
MatrixUtils.createRealMatrix(testData3x3NonSingular);
-        RealMatrix R = new QRDecompositionImpl(matrix).getR();
-        for (int i = 0; i < R.getRowDimension(); i++)
-            for (int j = 0; j < i; j++)
-                assertEquals("R lower triangle", R.getEntry(i, j), 0,
-                        entryTolerance);
+        checkUpperTriangular(new QRDecompositionImpl(matrix).getR());
 
         matrix = MatrixUtils.createRealMatrix(testData3x3Singular);
-        R = new QRDecompositionImpl(matrix).getR();
-        for (int i = 0; i < R.getRowDimension(); i++)
-            for (int j = 0; j < i; j++)
-                assertEquals("R lower triangle", R.getEntry(i, j), 0,
-                        entryTolerance);
+        checkUpperTriangular(new QRDecompositionImpl(matrix).getR());
 
         matrix = MatrixUtils.createRealMatrix(testData3x4);
-        R = new QRDecompositionImpl(matrix).getR();
-        for (int i = 0; i < R.getRowDimension(); i++)
-            for (int j = 0; j < i; j++)
-                assertEquals("R lower triangle", R.getEntry(i, j), 0,
-                        entryTolerance);
+        checkUpperTriangular(new QRDecompositionImpl(matrix).getR());
 
         matrix = MatrixUtils.createRealMatrix(testData4x3);
-        R = new QRDecompositionImpl(matrix).getR();
-        for (int i = 0; i < R.getRowDimension(); i++)
-            for (int j = 0; j < i; j++)
-                assertEquals("R lower triangle", R.getEntry(i, j), 0,
-                        entryTolerance);
+        checkUpperTriangular(new QRDecompositionImpl(matrix).getR());
+
+        Random r = new Random(643895747384642l);
+        int    p = (5 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        int    q = (7 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        matrix = createTestMatrix(r, p, q);
+        checkUpperTriangular(new QRDecompositionImpl(matrix).getR());
+
+        matrix = createTestMatrix(r, p, q);
+        checkUpperTriangular(new QRDecompositionImpl(matrix).getR());
+
+    }
+
+    private void checkUpperTriangular(RealMatrix m) {
+        m.walkInOptimizedOrder(new DefaultRealMatrixPreservingVisitor() {
+            private static final long serialVersionUID = -7685630069569815930L;
+            public void visit(int row, int column, double value) {
+                if (column < row) {
+                    assertEquals(0.0, value, entryTolerance);
+                }
+            }
+        });
     }
 
     /** test that H is trapezoidal */
     public void testHTrapezoidal() {
         RealMatrix matrix = 
MatrixUtils.createRealMatrix(testData3x3NonSingular);
-        RealMatrix H = new QRDecompositionImpl(matrix).getH();
-        for (int i = 0; i < H.getRowDimension(); i++)
-            for (int j = i + 1; j < H.getColumnDimension(); j++)
-                assertEquals(H.getEntry(i, j), 0, entryTolerance);
+        checkTrapezoidal(new QRDecompositionImpl(matrix).getH());
 
         matrix = MatrixUtils.createRealMatrix(testData3x3Singular);
-        H = new QRDecompositionImpl(matrix).getH();
-        for (int i = 0; i < H.getRowDimension(); i++)
-            for (int j = i + 1; j < H.getColumnDimension(); j++)
-                assertEquals(H.getEntry(i, j), 0, entryTolerance);
+        checkTrapezoidal(new QRDecompositionImpl(matrix).getH());
 
         matrix = MatrixUtils.createRealMatrix(testData3x4);
-        H = new QRDecompositionImpl(matrix).getH();
-        for (int i = 0; i < H.getRowDimension(); i++)
-            for (int j = i + 1; j < H.getColumnDimension(); j++)
-                assertEquals(H.getEntry(i, j), 0, entryTolerance);
+        checkTrapezoidal(new QRDecompositionImpl(matrix).getH());
 
         matrix = MatrixUtils.createRealMatrix(testData4x3);
-        H = new QRDecompositionImpl(matrix).getH();
-        for (int i = 0; i < H.getRowDimension(); i++)
-            for (int j = i + 1; j < H.getColumnDimension(); j++)
-                assertEquals(H.getEntry(i, j), 0, entryTolerance);
+        checkTrapezoidal(new QRDecompositionImpl(matrix).getH());
+
+        Random r = new Random(643895747384642l);
+        int    p = (5 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        int    q = (7 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        matrix = createTestMatrix(r, p, q);
+        checkTrapezoidal(new QRDecompositionImpl(matrix).getH());
+
+        matrix = createTestMatrix(r, p, q);
+        checkTrapezoidal(new QRDecompositionImpl(matrix).getH());
 
     }
 
+    private void checkTrapezoidal(RealMatrix m) {
+        m.walkInOptimizedOrder(new DefaultRealMatrixPreservingVisitor() {
+            private static final long serialVersionUID = -43649044361860701L;
+            public void visit(int row, int column, double value) {
+                if (column > row) {
+                    assertEquals(0.0, value, entryTolerance);
+                }
+            }
+        });
+    }
     /** test matrices values */
     public void testMatricesValues() {
         QRDecomposition qr =
@@ -233,4 +244,16 @@
         
     }
 
+    private RealMatrix createTestMatrix(final Random r, final int rows, final 
int columns) {
+        RealMatrix m = MatrixUtils.createRealMatrix(rows, columns);
+        m.walkInOptimizedOrder(new DefaultRealMatrixChangingVisitor(){
+            private static final long serialVersionUID = -556118291433400034L;
+            public double visit(int row, int column, double value)
+                throws MatrixVisitorException {
+                return 2.0 * r.nextDouble() - 1.0;
+            }
+        });
+        return m;
+    }
+
 }

Modified: 
commons/proper/math/trunk/src/test/org/apache/commons/math/linear/QRSolverTest.java
URL: 
http://svn.apache.org/viewvc/commons/proper/math/trunk/src/test/org/apache/commons/math/linear/QRSolverTest.java?rev=731308&r1=731307&r2=731308&view=diff
==============================================================================
--- 
commons/proper/math/trunk/src/test/org/apache/commons/math/linear/QRSolverTest.java
 (original)
+++ 
commons/proper/math/trunk/src/test/org/apache/commons/math/linear/QRSolverTest.java
 Sun Jan  4 10:08:50 2009
@@ -17,31 +17,37 @@
 
 package org.apache.commons.math.linear;
 
+import java.util.Random;
+
 import junit.framework.Test;
 import junit.framework.TestCase;
 import junit.framework.TestSuite;
 
 public class QRSolverTest extends TestCase {
     double[][] testData3x3NonSingular = { 
-            { 12, -51, 4 }, 
-            { 6, 167, -68 },
-            { -4, 24, -41 }, };
+            { 12, -51,   4 }, 
+            {  6, 167, -68 },
+            { -4,  24, -41 }
+    };
 
     double[][] testData3x3Singular = { 
-            { 1, 4, 7, }, 
-            { 2, 5, 8, },
-            { 3, 6, 9, }, };
+            { 1, 2,  2 }, 
+            { 2, 4,  6 },
+            { 4, 8, 12 }
+    };
 
     double[][] testData3x4 = { 
-            { 12, -51, 4, 1 }, 
-            { 6, 167, -68, 2 },
-            { -4, 24, -41, 3 }, };
+            { 12, -51,   4, 1 }, 
+            {  6, 167, -68, 2 },
+            { -4,  24, -41, 3 }
+    };
 
     double[][] testData4x3 = { 
-            { 12, -51, 4, }, 
-            { 6, 167, -68, },
-            { -4, 24, -41, }, 
-            { -5, 34, 7, }, };
+            { 12, -51,   4 }, 
+            {  6, 167, -68 },
+            { -4,  24, -41 }, 
+            { -5,  34,   7 }
+    };
 
     public QRSolverTest(String name) {
         super(name);
@@ -55,25 +61,25 @@
 
     /** test rank */
     public void testRank() {
-        QRSolver solver =
-            new QRSolver(new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData3x3NonSingular)));
+        DecompositionSolver solver =
+            new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData3x3NonSingular)).getSolver();
         assertTrue(solver.isNonSingular());
 
-        solver = new QRSolver(new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData3x3Singular)));
+        solver = new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData3x3Singular)).getSolver();
         assertFalse(solver.isNonSingular());
 
-        solver = new QRSolver(new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData3x4)));
-        assertFalse(solver.isNonSingular());
+        solver = new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData3x4)).getSolver();
+        assertTrue(solver.isNonSingular());
 
-        solver = new QRSolver(new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData4x3)));
+        solver = new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData4x3)).getSolver();
         assertTrue(solver.isNonSingular());
 
     }
 
     /** test solve dimension errors */
     public void testSolveDimensionErrors() {
-        QRSolver solver =
-            new QRSolver(new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData3x3NonSingular)));
+        DecompositionSolver solver =
+            new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData3x3NonSingular)).getSolver();
         RealMatrix b = MatrixUtils.createRealMatrix(new double[2][2]);
         try {
             solver.solve(b);
@@ -103,8 +109,8 @@
 
     /** test solve rank errors */
     public void testSolveRankErrors() {
-        QRSolver solver =
-            new QRSolver(new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData3x3Singular)));
+        DecompositionSolver solver =
+            new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData3x3Singular)).getSolver();
         RealMatrix b = MatrixUtils.createRealMatrix(new double[3][2]);
         try {
             solver.solve(b);
@@ -134,8 +140,9 @@
 
     /** test solve */
     public void testSolve() {
-        QRSolver solver =
-            new QRSolver(new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData3x3NonSingular)));
+        QRDecomposition decomposition =
+            new 
QRDecompositionImpl(MatrixUtils.createRealMatrix(testData3x3NonSingular));
+        DecompositionSolver solver = decomposition.getSolver();
         RealMatrix b = MatrixUtils.createRealMatrix(new double[][] {
                 { -102, 12250 }, { 544, 24500 }, { 167, -36750 }
         });
@@ -144,31 +151,82 @@
         });
 
         // using RealMatrix
-        assertEquals(0, solver.solve(b).subtract(xRef).getNorm(), 1.0e-13);
+        assertEquals(0, solver.solve(b).subtract(xRef).getNorm(), 2.0e-16 * 
xRef.getNorm());
 
         // using double[]
         for (int i = 0; i < b.getColumnDimension(); ++i) {
-            assertEquals(0,
-                         new 
RealVectorImpl(solver.solve(b.getColumn(i))).subtract(xRef.getColumnVector(i)).getNorm(),
-                         1.0e-13);
+            final double[] x = solver.solve(b.getColumn(i));
+            final double error = new 
RealVectorImpl(x).subtract(xRef.getColumnVector(i)).getNorm();
+            assertEquals(0, error, 3.0e-16 * 
xRef.getColumnVector(i).getNorm());
         }
 
         // using RealVectorImpl
         for (int i = 0; i < b.getColumnDimension(); ++i) {
-            assertEquals(0,
-                         
solver.solve(b.getColumnVector(i)).subtract(xRef.getColumnVector(i)).getNorm(),
-                         1.0e-13);
+            final RealVector x = solver.solve(b.getColumnVector(i));
+            final double error = x.subtract(xRef.getColumnVector(i)).getNorm();
+            assertEquals(0, error, 3.0e-16 * 
xRef.getColumnVector(i).getNorm());
         }
 
         // using RealVector with an alternate implementation
         for (int i = 0; i < b.getColumnDimension(); ++i) {
             RealVectorImplTest.RealVectorTestImpl v =
                 new RealVectorImplTest.RealVectorTestImpl(b.getColumn(i));
-            assertEquals(0,
-                         
solver.solve(v).subtract(xRef.getColumnVector(i)).getNorm(),
-                         1.0e-13);
+            final RealVector x = solver.solve(v);
+            final double error = x.subtract(xRef.getColumnVector(i)).getNorm();
+            assertEquals(0, error, 3.0e-16 * 
xRef.getColumnVector(i).getNorm());
         }
 
     }
 
+    public void testOverdetermined() {
+        final Random r    = new Random(5559252868205245l);
+        int          p    = (7 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        int          q    = (5 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        RealMatrix   a    = createTestMatrix(r, p, q);
+        RealMatrix   xRef = createTestMatrix(r, q, DenseRealMatrix.BLOCK_SIZE 
+ 3);
+
+        // build a perturbed system: A.X + noise = B
+        RealMatrix b = a.multiply(xRef);
+        final double noise = 0.001;
+        b.walkInOptimizedOrder(new DefaultRealMatrixChangingVisitor() {
+            private static final long serialVersionUID = 3533849820776962636L;
+            public double visit(int row, int column, double value) {
+                return value * (1.0 + noise * (2 * r.nextDouble() - 1));
+            }
+        });
+
+        // despite perturbation, the least square solution should be pretty 
good
+        RealMatrix x = new QRDecompositionImpl(a).getSolver().solve(b);
+        assertEquals(0, x.subtract(xRef).getNorm(), 0.01 * noise * p * q);
+
+    }
+
+    public void testUnderdetermined() {
+        final Random r    = new Random(42185006424567123l);
+        int          p    = (5 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        int          q    = (7 * DenseRealMatrix.BLOCK_SIZE) / 4;
+        RealMatrix   a    = createTestMatrix(r, p, q);
+        RealMatrix   xRef = createTestMatrix(r, q, DenseRealMatrix.BLOCK_SIZE 
+ 3);
+        RealMatrix   b    = a.multiply(xRef);
+        RealMatrix   x = new QRDecompositionImpl(a).getSolver().solve(b);
+
+        // too many equations, the system cannot be solved at all
+        assertTrue(x.subtract(xRef).getNorm() / (p * q) > 0.01);
+
+        // the last unknown should have been set to 0
+        assertEquals(0.0, x.getSubMatrix(p, q - 1, 0, x.getColumnDimension() - 
1).getNorm());
+
+    }
+
+    private RealMatrix createTestMatrix(final Random r, final int rows, final 
int columns) {
+        RealMatrix m = MatrixUtils.createRealMatrix(rows, columns);
+        m.walkInOptimizedOrder(new DefaultRealMatrixChangingVisitor(){
+            private static final long serialVersionUID = -556118291433400034L;
+            public double visit(int row, int column, double value)
+                throws MatrixVisitorException {
+                return 2.0 * r.nextDouble() - 1.0;
+            }
+        });
+        return m;
+    }
 }


Reply via email to