blob: 11ac70132d1dbaa2350a092a1381ac1dc742d8fd [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.hardware.cts;
import junit.framework.Assert;
import android.content.Context;
import android.hardware.SensorManager;
import android.os.PowerManager;
import java.util.Random;
public class SensorManagerStaticTest extends SensorTestCase {
private static final String TAG = "SensorManagerTest";
// local float version of PI
private static final float FLOAT_PI = (float) Math.PI;
private PowerManager.WakeLock mWakeLock;
@Override
protected void setUp() throws Exception {
Context context = getContext();
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
mWakeLock.acquire();
}
@Override
protected void tearDown(){
if (mWakeLock != null && mWakeLock.isHeld()) {
mWakeLock.release();
}
}
// SensorManager Tests
public void testGetAltitude() throws Exception {
float r, q;
float altitude;
// identity property
for (r = 0.5f; r < 1.3f; r += 0.1f) {
altitude = SensorManager.getAltitude(r * SensorManager.PRESSURE_STANDARD_ATMOSPHERE,
r * SensorManager.PRESSURE_STANDARD_ATMOSPHERE);
assertRoughlyEqual("getAltitude identity property violated.", altitude, 0.0f, 0.1f);
}
// uniform increasing as pressure decreases property
float prevAltitude = 1e5f; // 100km ceiling
for (r = 0.5f; r < 1.3f; r += 0.01f) {
altitude = SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE,
r * SensorManager.PRESSURE_STANDARD_ATMOSPHERE);
assertTrue("getAltitude result has to decrease as p increase.", prevAltitude > altitude);
prevAltitude = altitude;
}
// compare to a reference algorithm
final float coef = 1.0f / 5.255f;
for (r = 0.8f; r < 1.3f; r += 0.1f) {
for (q = 1.1f * r; q > 0.5f * r; q -= 0.1f * r) {
float p0 = r * SensorManager.PRESSURE_STANDARD_ATMOSPHERE;
float p = q * SensorManager.PRESSURE_STANDARD_ATMOSPHERE;
float t1 = SensorManager.getAltitude(p0, p);
float t2 = 44330.f*(1.0f- (float) Math.pow(p/p0, coef));
assertRoughlyEqual(
String.format("getAltitude comparing to reference algorithm failed. " +
"Detail: getAltitude(%f, %f) => %f, reference => %f",
p0, p, t1, t2),
t1, t2, 100.f);
}
}
}
public void testGetAngleChange() throws Exception {
TestDataGenerator data = new TestDataGenerator();
int i;
float [] rotv = new float[3];
float [] rotv2 = new float[3];
// test many instances
for (i=0; i<100; ++i) {
float [] R1, R12, R2;
// azimuth(yaw) pitch roll
data.nextRotationAngles(rotv);
R1 = mat9VRot(rotv); // random base
// azimuth(yaw) pitch roll
data.nextRotationAngles(rotv);
R12 = mat9VRot(rotv);
R2 = mat9Mul(R1, R12); // apply another random rotation
// test different variations of input matrix format
switch(i & 3) {
case 0:
SensorManager.getAngleChange(rotv2, R2, R1);
break;
case 1:
SensorManager.getAngleChange(rotv2, mat9to16(R2), R1);
break;
case 2:
SensorManager.getAngleChange(rotv2, R2, mat9to16(R1));
break;
case 3:
SensorManager.getAngleChange(rotv2, mat9to16(R2), mat9to16(R1));
break;
}
// check range
assertRotationAnglesValid("getAngleChange result out of range.", rotv2);
// avoid directly checking the rotation angles to avoid corner cases
float [] R12rt = mat9T(mat9VRot(rotv2));
float [] RI = mat9Mul(R12rt, R12);
assertRoughlyEqual(
String.format("getAngleChange result is incorrect. Details: case %d, " +
"truth = [%f, %f, %f], result = [%f, %f, %f]", i, rotv[0], rotv[1], rotv[2],
rotv2[0], rotv2[1], rotv2[2]),
RI[0] + RI[4] + RI[8], 3.f, 1e-4f);
}
}
public void testGetInclination() throws Exception {
TestDataGenerator data = new TestDataGenerator();
int i;
float [] rotv = new float[3];
float [] rotv2 = new float[3];
float [] rotv3;
// test many instances
for (i = 0; i < 100; ++i) {
float [] R;
float angle;
angle = (data.nextFloat()-0.5f) * FLOAT_PI;
R = mat9Rot(SensorManager.AXIS_X, -angle);
float angler = ((i&1) != 0) ?
SensorManager.getInclination(mat9to16(R)) : SensorManager.getInclination(R);
assertRoughlyEqual(
String.format(
"getInclination return incorrect result. Detail: case %d, truth %f, result %f.",
i, angle, angler),
angle, angler, 1e-4f);
}
}
public void testGetOrientation() throws Exception {
TestDataGenerator data = new TestDataGenerator();
int i;
float [] rotv = new float[3];
float [] rotv2 = new float[3];
float [] rotv3;
// test many instances
for (i=0; i<100; ++i) {
float [] R;
// yaw pitch roll
data.nextRotationAngles(rotv);
R = mat9VRot(rotv);
rotv3 = SensorManager.getOrientation( ((i&1) != 0) ? R : mat9to16(R), rotv2);
assertTrue("getOrientaion has to return the array passed in argument", rotv3 == rotv2);
// check range
assertRotationAnglesValid("getOrientation result out of range.", rotv2);
// Avoid directly comparing rotation angles. Instead, compare the rotation matrix.
float [] Rr = mat9T(mat9VRot(rotv2));
float [] RI = mat9Mul(Rr, R);
assertRoughlyEqual(
String.format("getOrientation result is incorrect. Details: case %d, " +
"truth = [%f, %f, %f], result = [%f, %f, %f]", i, rotv[0], rotv[1], rotv[2],
rotv2[0], rotv2[1], rotv2[2]),
RI[0] + RI[4] + RI[8], 3.f, 1e-4f);
}
}
public void testGetQuaternionFromVector() throws Exception {
TestDataGenerator data = new TestDataGenerator();
int i;
float [] v;
float [] q = new float[4];
float [] q2 = new float[4];
float [] v3 = new float[3];
float [] v4 = new float[4];
float [] v5 = new float[5];
float [][] vs = new float[][] {v3, v4, v5};
float [] xyzth = new float[4];
for (i = 0; i < 100; ++i) {
float c, s;
data.nextRotationAxisAngle(xyzth);
c = (float) Math.cos(xyzth[3]);
s = (float) Math.sin(xyzth[3]);
if (c < 0.f) {
c = -c;
s = -s;
}
v = vs[i%3];
switch(i%3) {
case 2:
v[4] = data.nextBoolean() ? data.nextFloat() : -1.f;
case 1:
v[3] = c;
case 0:
v[0] = s * xyzth[0];
v[1] = s * xyzth[1];
v[2] = s * xyzth[2];
}
q2[0] = c;
q2[1] = v[0];
q2[2] = v[1];
q2[3] = v[2];
SensorManager.getQuaternionFromVector(q, v);
assertVectorRoughlyEqual(
String.format("getQuaternionFromVector returns wrong results, Details: case %d, " +
"truth = (%f, %f, %f, %f), result = (%f, %f, %f, %f).",
i, q2[0], q2[1], q2[2], q2[3], q[0], q[1], q[2], q[3]),
q, q2, 1e-4f);
}
}
public void testGetRotationMatrix() throws Exception {
TestDataGenerator data = new TestDataGenerator();
final float gravity = 9.81f;
final float magStrength = 50.f;
int i;
float [] gm = new float[9];
float [] rotv = new float[3];
float [] gI = null;
float [] mI = null;
float [] Rr = new float[9];
float [] Ir = new float[9];
gm[6] = gravity; // m/s^2, first column gravity
// test many instances
for (i=0; i<100; ++i) {
float [] Rt;
float incline;
// yaw pitch roll
data.nextRotationAngles(rotv);
Rt = mat9T(mat9VRot(rotv)); // from world frame to phone frame
//Rt = mat9I();
incline = -0.9f * (data.nextFloat() - 0.5f) * FLOAT_PI; // ~ +-80 degrees
//incline = 0.f;
gm[4] = magStrength * (float) Math.cos(-incline); // positive means rotate downwards
gm[7] = magStrength * (float) Math.sin(-incline);
float [] gmb = mat9Mul(Rt, gm); // do not care about right most column
gI = mat9Axis(gmb, SensorManager.AXIS_X);
mI = mat9Axis(gmb, SensorManager.AXIS_Y);
assertTrue("getRotationMatrix returns false on valid inputs",
SensorManager.getRotationMatrix(Rr, Ir, gI, mI));
float [] n = mat9Mul(Rr, Rt);
assertRoughlyEqual(
String.format("getRotationMatrix returns incorrect R matrix. " +
"Details: case %d, truth R = %s, result R = %s.",
i, mat9ToStr(mat9T(Rt)), mat9ToStr(Rr)),
n[0] + n[4] + n[8], 3.f, 1e-4f);
// Magnetic incline is defined so that it means the magnetic field lines is formed
// by rotate local y axis around -x axis by incline angle. However, I matrix is
// defined as (according to document):
// [0 m 0] = I * R * geomagnetic,
// which means,
// I' * [0 m 0] = R * geomagnetic.
// Thus, I' = Rot(-x, incline) and I = Rot(-x, incline)' = Rot(x, incline)
float [] Ix = mat9Rot(SensorManager.AXIS_X, incline);
assertVectorRoughlyEqual(
String.format("getRotationMatrix returns incorrect I matrix. " +
"Details: case %d, truth I = %s, result I = %s.",
i, mat9ToStr(Ix), mat9ToStr(Ir)),
Ix, Ir, 1e-4f);
}
// test 16 element inputs
float [] Rr2 = new float[16];
float [] Ir2 = new float[16];
assertTrue("getRotationMatrix returns false on valid inputs",
SensorManager.getRotationMatrix(Rr2, Ir2, gI, mI));
assertVectorRoughlyEqual(
"getRotationMatrix acts inconsistent with 9- and 16- elements matrix buffer",
mat16to9(Rr2), Rr, 1e-4f);
assertVectorRoughlyEqual(
"getRotationMatrix acts inconsistent with 9- and 16- elements matrix buffer",
mat16to9(Ir2), Ir, 1e-4f);
// test null inputs
assertTrue("getRotationMatrix does not handle null inputs",
SensorManager.getRotationMatrix(Rr, null, gI, mI));
assertTrue("getRotationMatrix does not handle null inputs",
SensorManager.getRotationMatrix(null, Ir, gI, mI));
assertTrue("getRotationMatrix does not handle null inputs",
SensorManager.getRotationMatrix(null, null, gI, mI));
// test fail cases
// free fall, if the acc reading is less than 10% of gravity
gI[0] = gI[1] = gI[2] = data.nextFloat() * gravity * 0.05f; // sqrt(3) * 0.05 < 0.1
assertFalse("getRotationMatrix does not fail when it supposed to fail (gravity too small)",
SensorManager.getRotationMatrix(Rr, Ir, gI, mI));
// wrong input
assertFalse("getRotationMatrix does not fail when it supposed to fail (singular axis)",
SensorManager.getRotationMatrix(Rr, Ir, gI, gI));
}
public void testGetRotationMatrixFromVector() throws Exception {
TestDataGenerator data = new TestDataGenerator();
int i;
float [] v;
float [] q = new float[4];
float [] v3 = new float[3];
float [] v4 = new float[4];
float [] v5 = new float[5];
float [][] vs = new float[][]{v3, v4, v5};
float [] m9 = new float[9];
float [] m16 = new float[16];
// format: x y z theta/2
float [] xyzth = new float[4];
// test the orthogonal property of returned matrix
for (i=0; i<20; ++i) {
float c, s;
data.nextRotationAxisAngle(xyzth);
c = (float) Math.cos(xyzth[3]);
s = (float) Math.sin(xyzth[3]);
if (c < 0.f) {
c = -c;
s = -s;
}
v = vs[i%3];
switch(i%3) {
case 2:
v[4] = data.nextBoolean() ? data.nextFloat() : -1.f;
case 1:
v[3] = c;
case 0:
v[0] = s * xyzth[0];
v[1] = s * xyzth[1];
v[2] = s * xyzth[2];
}
if ((i % 1) != 0) {
SensorManager.getRotationMatrixFromVector(m16, v);
m9 = mat16to9(m16);
}else {
SensorManager.getRotationMatrixFromVector(m9, v);
}
float [] n = mat9Mul(m9, mat9T(m9));
assertRoughlyEqual("getRotationMatrixFromVector do not return proper matrix",
n[0]+ n[4] + n[8], 3.f, 1e-4f);
}
// test if multiple rotation (total 2pi) about an axis result in identity
v = v3;
float [] Rr = new float[9];
for (i=0; i<20; ++i) {
float j, halfTheta, residualHalfTheta = FLOAT_PI;
float [] R = mat9I();
float c, s;
data.nextRotationAxisAngle(xyzth); // half theta is ignored
j = data.nextInt(5) + 2; // 2 ~ 6 rotations
while(j-- > 0) {
if (j == 0) {
halfTheta = residualHalfTheta;
} else {
halfTheta = data.nextFloat() * FLOAT_PI;
}
c = (float) Math.cos(halfTheta);
s = (float) Math.sin(halfTheta);
if (c < 0.f) {
c = -c;
s = -s;
}
v[0] = s * xyzth[0];
v[1] = s * xyzth[1];
v[2] = s * xyzth[2];
SensorManager.getRotationMatrixFromVector(Rr, v);
R = mat9Mul(Rr, R);
residualHalfTheta -= halfTheta;
}
assertRoughlyEqual("getRotationMatrixFromVector returns incorrect matrix",
R[0] + R[4] + R[8], 3.f, 1e-4f);
}
// test if rotation about trival axis works
v = v3;
for (i=0; i<20; ++i) {
int axis = (i % 3) + 1;
float theta = data.nextFloat() * 2.f * FLOAT_PI;
float [] R;
v[0] = v[1] = v[2] = 0.f;
v[axis - 1] = (float) Math.sin(theta / 2.f);
if ( (float) Math.cos(theta / 2.f) < 0.f) {
v[axis-1] = -v[axis-1];
}
SensorManager.getRotationMatrixFromVector(m9, v);
R = mat9Rot(axis, theta);
assertVectorRoughlyEqual(
String.format("getRotationMatrixFromVector returns incorrect matrix with "+
"simple rotation. Details: case %d, truth R = %s, result R = %s.",
i, mat9ToStr(R), mat9ToStr(m9)),
R, m9, 1e-4f);
}
}
public void testRemapCoordinateSystem() throws Exception {
TestDataGenerator data = new TestDataGenerator();
int i, j, k;
float [] rotv = new float[3];
float [] Rout = new float[9];
float [] Rout2 = new float[16];
int a1, a2; // AXIS_X/Y/Z
int b1, b2, b3; // AXIS_X/Y/Z w/ or w/o MINUS
// test a few instances
for (i=0; i<10; ++i) {
float [] R;
// yaw pitch roll
data.nextRotationAngles(rotv);
R = mat9VRot(rotv);
// total of 6*4 = 24 variations
// 6 = A(3,2)
for (j=0; j<9; ++j) {
// axis without minus
a1 = j/3 + 1;
a2 = j%3 + 1;
// skip cases when two axis are the same
if (a1 == a2) continue;
for (k=0; k<3; ++k) {
// test all minus axis combination: ++, +-, -+, --
b1 = a1 | (((k & 2) != 0) ? 0x80 : 0);
b2 = a2 | (((k & 1) != 0) ? 0x80 : 0);
// the third axis
b3 = (6 - a1 -a2) |
( (((a2 + 3 - a1) % 3 == 2) ? 0x80 : 0) ^ (b1 & 0x80) ^ (b2 & 0x80));
// test both input formats
if ( (i & 1) != 0 ) {
assertTrue(SensorManager.remapCoordinateSystem(R, b1, b2, Rout));
} else {
assertTrue(SensorManager.remapCoordinateSystem(mat9to16(R), b1, b2, Rout2));
Rout = mat16to9(Rout2);
}
float [] v1, v2;
String detail = String.format(
"Details: case %d (%x %x %x), original R = %s, result R = %s.",
i, b1, b2, b3, mat9ToStr(R), mat9ToStr(Rout));
v1 = mat9Axis(R, SensorManager.AXIS_X);
v2 = mat9Axis(Rout, b1);
assertVectorRoughlyEqual(
"remapCoordinateSystem gives incorrect result (x)." + detail,
v1, v2, 1e-4f);
v1 = mat9Axis(R, SensorManager.AXIS_Y);
v2 = mat9Axis(Rout, b2);
assertVectorRoughlyEqual(
"remapCoordinateSystem gives incorrect result (y)." + detail,
v1, v2, 1e-4f);
v1 = mat9Axis(R, SensorManager.AXIS_Z);
v2 = mat9Axis(Rout, b3);
assertVectorRoughlyEqual(
"remapCoordinateSystem gives incorrect result (z)." + detail,
v1, v2, 1e-4f);
}
}
}
// test cases when false should be returned
assertTrue("remapCoordinateSystem should return false with mismatch size input and output",
!SensorManager.remapCoordinateSystem(Rout,
SensorManager.AXIS_Y, SensorManager.AXIS_Z, Rout2));
assertTrue("remapCoordinateSystem should return false with invalid axis setting",
!SensorManager.remapCoordinateSystem(Rout,
SensorManager.AXIS_X, SensorManager.AXIS_X, Rout));
assertTrue("remapCoordinateSystem should return false with invalid axis setting",
!SensorManager.remapCoordinateSystem(Rout,
SensorManager.AXIS_X, SensorManager.AXIS_MINUS_X, Rout));
}
// Utilities class & functions
private class TestDataGenerator {
// carry out test deterministically without manually picking numbers
private final long DEFAULT_SEED = 0xFEDCBA9876543210l;
private Random mRandom;
TestDataGenerator(long seed) {
mRandom = new Random(seed);
}
TestDataGenerator() {
mRandom = new Random(DEFAULT_SEED);
}
void nextRotationAngles(float [] rotv) {
assertTrue(rotv.length == 3);
rotv[0] = (mRandom.nextFloat()-0.5f) * 2.0f * FLOAT_PI; // azimuth(yaw) -pi ~ pi
rotv[1] = (mRandom.nextFloat()-0.5f) * FLOAT_PI; // pitch -pi/2 ~ +pi/2
rotv[2] = (mRandom.nextFloat()-0.5f) * 2.f * FLOAT_PI; // roll -pi ~ +pi
}
void nextRotationAxisAngle(float [] aa) {
assertTrue(aa.length == 4);
aa[0] = (mRandom.nextFloat() - 0.5f) * 2.f;
aa[1] = (mRandom.nextFloat() - 0.5f ) * 2.f * (float) Math.sqrt(1.f - aa[0] * aa[0]);
aa[2] = (mRandom.nextBoolean() ? 1.f : -1.f) *
(float) Math.sqrt(1.f - aa[0] * aa[0] - aa[1] * aa[1]);
aa[3] = mRandom.nextFloat() * FLOAT_PI;
}
int nextInt(int i) {
return mRandom.nextInt(i);
}
float nextFloat() {
return mRandom.nextFloat();
}
boolean nextBoolean() {
return mRandom.nextBoolean();
}
}
private static void assertRotationAnglesValid(String message, float[] ra) {
assertTrue(message, ra.length == 3 &&
ra[0] >= -FLOAT_PI && ra[0] <= FLOAT_PI && // azimuth
ra[1] >= -FLOAT_PI / 2.f && ra[1] <= FLOAT_PI / 2.f && // pitch
ra[2] >= -FLOAT_PI && ra[2] <= FLOAT_PI); // roll
}
private static void assertRoughlyEqual(String message, float a, float b, float bound) {
assertTrue(message, Math.abs(a-b) < bound);
}
private static void assertVectorRoughlyEqual(String message, float [] v1, float [] v2,
float bound) {
assertTrue(message, v1.length == v2.length);
int i;
float sum = 0.f;
for (i=0; i<v1.length; ++i) {
sum += (v1[i] - v2[i]) * (v1[i] - v2[i]);
}
assertRoughlyEqual(message, (float)Math.sqrt(sum), 0.f, bound);
}
private static float [] mat9to16(float [] m) {
assertTrue(m.length == 9);
float [] n = new float[16];
int i;
for (i=0; i<9; ++i) {
n[i+i/3] = m[i];
}
n[15] = 1.f;
return n;
}
private static float [] mat16to9(float [] m) {
assertTrue(m.length == 16);
float [] n = new float[9];
int i;
for (i=0; i<9; ++i) {
n[i] = m[i + i/3];
}
return n;
}
private static float [] mat9Mul(float [] m, float [] n) {
assertTrue(m.length == 9 && n.length == 9);
float [] r = new float[9];
int i, j, k;
for (i = 0; i < 3; ++i)
for (j = 0; j < 3; ++j)
for (k = 0; k < 3; ++k)
r[i * 3 + j] += m[i * 3 + k] * n[k * 3 + j];
return r;
}
private static float [] mat9T(float [] m) {
assertTrue(m.length == 9);
int i, j;
float [] n = new float[9];
for (i = 0; i < 3; ++i)
for (j = 0; j < 3; ++j)
n[i * 3 + j] = m[j * 3 + i];
return n;
}
private static float [] mat9I() {
float [] m = new float[9];
m[0] = m[4] = m[8] = 1.f;
return m;
}
private static float [] mat9Rot(int axis, float angle) {
float [] m = new float[9];
switch (axis) {
case SensorManager.AXIS_X:
m[0] = 1.f;
m[4] = m[8] = (float) Math.cos(angle);
m[5] = - (m[7] = (float) Math.sin(angle));
break;
case SensorManager.AXIS_Y:
m[4] = 1.f;
m[0] = m[8] = (float) Math.cos(angle);
m[6] = - (m[2] = (float) Math.sin(angle));
break;
case SensorManager.AXIS_Z:
m[8] = 1.f;
m[0] = m[4] = (float) Math.cos(angle);
m[1] = - (m[3] = (float) Math.sin(angle));
break;
default:
// should never be here
assertTrue(false);
}
return m;
}
private static float [] mat9VRot(float [] angles) {
assertTrue(angles.length == 3);
// yaw, android yaw rotate to -z
float [] R = mat9Rot(SensorManager.AXIS_Z, -angles[0]);
// pitch, android pitch rotate to -x
R = mat9Mul(R, mat9Rot(SensorManager.AXIS_X, -angles[1]));
// roll
R = mat9Mul(R, mat9Rot(SensorManager.AXIS_Y, angles[2]));
return R;
}
private static float [] mat9Axis(float m[], int axis) {
assertTrue(m.length == 9);
boolean negative = (axis & 0x80) != 0;
float [] v = new float[3];
int offset;
offset = (axis & ~0x80) - 1;
v[0] = negative ? -m[offset] : m[offset];
v[1] = negative ? -m[offset+3] : m[offset+3];
v[2] = negative ? -m[offset+6] : m[offset+6];
return v;
}
private static float vecInner(float u[], float v[]) {
assertTrue(u.length == v.length);
int i;
float sum = 0.f;
for (i=0; i < v.length; ++i) {
sum += u[i]*v[i];
}
return (float)Math.sqrt(sum);
}
private static String vecToStr(float u[]) {
int i;
String s;
switch (u.length) {
case 3:
return String.format("[%f, %f, %f]", u[0], u[1], u[2]);
case 4:
return String.format("(%f, %f, %f, %f)", u[0], u[1], u[2], u[3]);
default:
s = "[";
for (i = 0; i < u.length-1; ++i) {
s += String.format("%f, ", u[i]);
}
s += String.format("%f]", u[i]);
return s;
}
}
private static String mat9ToStr(float m[]) {
assertTrue(m.length == 9);
return String.format("[%f, %f, %f; %f, %f, %f; %f, %f, %f]",
m[0], m[1], m[2],
m[3], m[4], m[5],
m[6], m[7], m[8]);
}
private static String mat16ToStr(float m[]) {
assertTrue(m.length == 16);
return String.format("[%f, %f, %f, %f; %f, %f, %f, %f; %f, %f, %f, %f; %f, %f, %f, %f]",
m[0], m[1], m[2], m[3],
m[4], m[5], m[6], m[7],
m[8], m[9], m[10], m[11],
m[12], m[13], m[14], m[15]);
}
}