/**************************************************************************** Copyright (c) 2003, Landmark Graphics and others. All rights reserved. This program and accompanying materials are made available under the terms of the Common Public License - v1.0, which accompanies this distribution, and is available at http://www.eclipse.org/legal/cpl-v10.html ****************************************************************************/ package com.lgc.wsh.opt; import java.util.logging.Logger; import com.lgc.wsh.util.Almost; /** For a linearized transform, implement the Gauss-Newton quadratic approximation of a damped least-squares objective function. Specifically,
      [f(m+x)-data]'N[f(m+x)-data] + (m+x)'M(m+x)
    
Linearization of
      f(m+x) ~= f(m) + Fx
    
makes the function quadratic in x
      [f(m)+Fx-data]'N[f(m)+Fx-data] + (m+x)'M(m+x)
    
If only perturbations are damped, then the objective function is
       [f(m)+Fx-data]'N[f(m)+Fx-data] + x'Mx
    
    m is the reference model, and x is the perturbation.
    f(m) is the nonlinear forward transform.
    F is the linearized version of f(m) with respect to the
    reference model m
    N is the inverse covariance of the data and
    M is the inverse covariance of the model.
    
The Hessian is given by
      H = F'NF + M
    
. b is determined by
       b = -F' N [data - f(m)] + Mm
    
for full damping, or by
      b = -F' N [data - f(m)]
    
if only perturbations are damped. @author W.S. Harlan */ public class TransformQuadratic implements Quadratic { private static final Logger LOG = Logger.getLogger("com.lgc.wsh.opt"); private VectConst _data; private VectConst _referenceModel; private VectConst _perturbModel; private Transform _transform; private boolean _dampOnlyPerturbation = false; /** Wrap known data, reference mode, and transform as a Gauss-Newton objectiveFunction. @param referenceModel This is the reference model m in the objective function. @param perturbModel If non-null, then use instances of this vector to perturb the reference model. It must be possible to project the referenceModel and perturbModel into each other. @param data The data to be fit. @param transform Optimize with this transform @param dampOnlyPerturbation If true then use objective function [f(m)+Fx-data]'N[f(m)+Fx-data] + x'Mx */ public TransformQuadratic(VectConst data, VectConst referenceModel, VectConst perturbModel, Transform transform, boolean dampOnlyPerturbation) { _data = data; _referenceModel = referenceModel; _perturbModel = perturbModel; _transform = transform; _dampOnlyPerturbation = dampOnlyPerturbation; } /** Run a few tests to ensure that transpose satisfies definition. This is expensive and intended only for test code. @return number of sigficant digits of precision in transpose. Expect 5 or 6. Unacceptable if not 3 or more. A value of 3 or 4 probably indicates a subtle error. */ public int getTransposePrecision() { VectConst d = _data; // arbitrary nonzero data Vect b = this.getB(); // arbitrary nonzero model double bb = b.dot(b); checkNaN(bb); assert ! Almost.FLOAT.zero(bb) : "Cannot test with zero-magnitude b"; // Get forward transform of b Vect Fb = VectUtil.cloneZero(d); Vect bSave = b.clone(); _transform.forwardLinearized(Fb, b, _referenceModel); // make sure didn't step on wrong vector assert VectUtil.areSame(b, bSave) :"model was changed by forward model"; bSave.dispose(); // check that forward zeros output data. Vect test = d.clone(); _transform.forwardLinearized(test, b, _referenceModel); assert VectUtil.areSame(test, Fb): "forwardLinearized should zero data"; test.dispose(); // Get transpose of d Vect Ad = VectUtil.cloneZero(b); Vect dSave = d.clone(); _transform.addTranspose(d, Ad,_referenceModel); double transposeMagnitude = Ad.dot(Ad); checkNaN(transposeMagnitude); // make sure didn't step on wrong vector assert VectUtil.areSame(d, dSave) : "data was changed by transpose"; dSave.dispose(); // ensure that transpose adds to existing model test = b.clone(); // make initial size comparable to transpose double scaleTest = 1.1*Math.sqrt(transposeMagnitude/bb); VectUtil.scale(test, scaleTest); _transform.addTranspose(d, test,_referenceModel); assert ! VectUtil.areSame(Ad, test): "Transpose should not zero model. "+ "Magnitude: b="+bb+ "trans="+transposeMagnitude+" test="+test.dot(test); test.add(1., -1., Ad); VectUtil.scale(test, 1./scaleTest); assert VectUtil.areSame(test, b) : "Transpose did not add to model vector"; test.dispose(); // get dot products that should be equal double dFb = d.dot(Fb); double Adb = Ad.dot(b); assert !Almost.FLOAT.zero(dFb) : "zero magnitude test: dFb is zero"; assert !Almost.FLOAT.zero(Adb) : "zero magnitude test: Adb is zero"; checkNaN(dFb); checkNaN(Adb); int significantDigits = 10; boolean matches = false; while (! matches && significantDigits > 0) { Almost almost = new Almost (significantDigits); matches = almost.equal(dFb, Adb); if (!matches) { --significantDigits; } } if (significantDigits < 3) { LOG.severe("Transpose precision is unacceptable: dFb="+dFb+" Adb="+Adb); } Ad.dispose(); Fb.dispose(); b.dispose(); return significantDigits; } /** Multiply by Hessian H = F'NF + M @param x Vector to be multiplied and modified */ public void multiplyHessian(Vect x) { Vect data = _data.clone(); _transform.forwardLinearized(data, x,_referenceModel); // data is Fx data.multiplyInverseCovariance(); // data is NFx x.multiplyInverseCovariance(); // x is Mx _transform.addTranspose(data, x,_referenceModel);// x is (F'NF +M)x data.dispose(); } // Quadratic public void inverseHessian(Vect x) { _transform.inverseHessian(x, _referenceModel); } /** Return gradient term of quadratic. This instance will be in the vector space of the perturbModel, if provided. Otherwise, uses an instance of the referenceModel. @return Value of b = -F' N [data - f(m)] + Mm if dampOnlyPerturbation is false, and b = -F' N [data - f(m)] if dampOnlyPerturbation is true. */ public Vect getB() { Vect data = VectUtil.cloneZero(_data); // data is data with zeros _transform.forwardNonlinear(data, _referenceModel); // data is f(m) data.add(1., -1.,_data); // data is -e = -(d - f(m)) _transform.adjustRobustErrors(data); // (remove outliers from e) data.multiplyInverseCovariance(); // data is -Ne Vect b = null; if (_dampOnlyPerturbation) { if (_perturbModel != null) { b = VectUtil.cloneZero(_perturbModel); // b is 0 } else { b = VectUtil.cloneZero(_referenceModel); // b is 0 } } else { if (_perturbModel != null) { b = _perturbModel.clone(); b.project(0., 1., _referenceModel); // b is m } else { b = _referenceModel.clone(); // b is m } b.multiplyInverseCovariance(); // b is M m } _transform.addTranspose(data, b, _referenceModel); // b is -F'Ne [+ Mm] data.dispose(); return b; } /** Evaluate the full objective function without approximation. Provide a model in the vector space of the referenceModel. perturbModel is unused. Useful for line-search of best scale factor. If dampedPerturbation, evaluates
        [f(m)-data]'N[f(m)-data] + (m-m0)'M(m-m0)
      
where m0 is the reference model. Otherwise evaluates
        [f(m)-data]'N[f(m)-data] + m'M m
      
@param m Model to be evaluated. @return Value of objective function. */ public double evalFullObjectiveFunction(VectConst m) { Vect data = VectUtil.cloneZero(_data); // data is zeros Vect model = m.clone(); // model is m model.constrain(); // may have been done already _transform.forwardNonlinear(data, model); // data is f(m) data.add(1., -1., _data); // data is e = f(m) - data _transform.adjustRobustErrors(data); double eNe = data.magnitude(); // eNe is e'Ne checkNaN(eNe); data.dispose(); // damp perturbation if requested if (_dampOnlyPerturbation) { model.add(1., -1., _referenceModel); // model is (m-m0) } double mMm = model.magnitude(); // mMm is (m-m0)'M(m-m0) checkNaN(mMm); model.dispose(); return (eNe + mMm); } /** Abort if NaN's appear. @param value Abort if this value is a NaN. */ private static void checkNaN(double value) { if (value*0 != 0) { throw new IllegalStateException("Value is a NaN"); } } /** Free up internal cached vectors */ public void dispose() {} }