src/hamiltonian/AttractionPointConstraint.js

import SoftConstraint from "./SoftConstraint.js"
import ParameterChecker from "./ParameterChecker.js"


/**Implements bias of motion in the direction of a supplied "attraction point".
 * This constraint computes the cosine of the angle alpha between the direction
 * of the copy attempt (from source to target pixel), and the direction from the
 * source to the attraction point. This cosine is 1 if these directions are
 * aligned, 0 if they are perpendicular, and 1 if they are opposite.
 * We take the negative (so that deltaH is negative for a copy attempt in the
 * right direction), and modify the strength of this bias using the lambda
 * parameter. The constraint only acts on copy attempts *from* the cell that
 * is responding to the field; it does not take into account the target pixel
 * (except for its location to determine the direction of the copy attempt).
 *
 * The current implementation works for torus grids as long as the grid size in
 * each dimension is larger than a few pixels.
 *
 * Automatic adding of this constraint via the conf object is currently not
 * supported, so you will have to add this constraint using the
 * {@link CPM#add} method.
 *
 * @example
 * // Build a CPM and add the constraint
 * let CPM = require( "path/to/build" )
 * let C = new CPM.CPM( [200,200], { T : 20 } )
 * C.add( new CPM.AttractionPointConstraint( {
 * 	LAMBDA_ATTRACTIONPOINT : [0,100],
 * 	ATTRACTIONPOINT: [[0,0],[100,100]],
 * } ) )
 *
 * // We can even add a second one at a different location
 * C.add( new CPM.AttractionPointConstraint( {
 * 	LAMBDA_ATTRACTIONPOINT : [0,100],
 * 	ATTRACTIONPOINT: [50,50],
 * } ) )
 */
class AttractionPointConstraint extends SoftConstraint {

	/** The constructor of an AttractionPointConstraint requires a conf object
	 * with two parameters.
	 * @param {object} conf - parameter object for this constraint
	 * @param {PerKindNonNegative} conf.LAMBDA_ATTRACTIONPOINT - strength of
	 * the constraint per cellkind.
	 * @param {ArrayCoordinate} conf.ATTRACTIONPOINT coordinate of the
	 * attraction point.
	*/
	constructor( conf ){
		super( conf )		
	}
	
	/** This method checks that all required parameters are present in the
	 * bject supplied to the constructor, and that they are of the right format.
	 * It throws an error when this is not the case.*/
	confChecker(){
		let checker = new ParameterChecker( this.conf, this.C )
		checker.confCheckParameter( "LAMBDA_ATTRACTIONPOINT",
			"KindArray", "NonNegative" )
		
		// Custom check for the attractionpoint
		checker.confCheckPresenceOf( "ATTRACTIONPOINT" )
		let pt = this.conf["ATTRACTIONPOINT"]
		if( !checker.isCoordinate(pt) ){
			throw( "ATTRACTIONPOINT must be a coordinate array with the same " +
				"dimensions as the grid!" )
		}
	}

	/** Method to compute the Hamiltonian for this constraint.
	 * @param {IndexCoordinate} src_i - coordinate of the source pixel that
	 * tries to copy.
	 * @param {IndexCoordinate} tgt_i - coordinate of the target pixel the
	 * source is trying to copy into.
	 * @param {CellId} src_type - cellid of the source pixel.
	 * @param {CellId} tgt_type - cellid of the target pixel. This argument is
	 * not actually used but is given for consistency with other soft
	 * constraints; the CPM always calls this method with four arguments.
	 * @return {number} the change in Hamiltonian for this copy attempt and
	 * this constraint. */
	/* eslint-disable no-unused-vars*/
	deltaH( src_i, tgt_i, src_type, tgt_type ){

		// deltaH is only non-zero when the source pixel belongs to a cell with
		// an attraction point, so it does not act on copy attempts where the
		// background would invade the cell.
		let l = this.cellParameter("LAMBDA_ATTRACTIONPOINT", src_type )
		if( !l ){
			return 0
		}

		// To assess whether the copy attempt lies in the direction of the
		// attraction point, we must take into account whether the grid has
		// wrapped boundaries (torus; see below).
		let torus = this.C.conf.torus

		// tgt is the attraction point; p1 is the source location and p2 is
		// the location of the target pixel.
		let tgt = this.cellParameter("ATTRACTIONPOINT", src_type )
		let p1 = this.C.grid.i2p( src_i ), p2 = this.C.grid.i2p( tgt_i )

		// To bias a copy attempt p1 -> p2 in the direction of vector 'dir'.
		// r will contain the dot product of the copy attempt vector and the
		// vector pointing from the source pixel to the attraction point.
		// The copy attempt vector always has length one, but the vector to the
		// attraction point has a variable length that will be stored in ldir
		// (actually, we store the squared length).
		let r = 0., ldir = 0.

		// loops over the coordinates x,y,(z)
		for( let i = 0; i < p1.length ; i++ ){

			// compute the distance between the target and the current position
			// in this dimension, and add it in squared form to the total.
			let dir_i = tgt[i] - p1[i]
			ldir += dir_i * dir_i

			// similarly, the distance between the source and target pixel in this
			// dimension (direction of the copy attempt is from p1 to p2)
			let dx = p2[i] - p1[i]

			// we may have to correct for torus if a copy attempt crosses the
			// boundary.
			let si = this.C.extents[i]
			if( torus[i] ){
				// If distance is greater than half the grid size, correct the
				// coordinate.
				if( dx > si/2 ){
					dx -= si
				} else if( dx < -si/2 ){
					dx += si
				}
			}

			// direction of the gradient; add contribution of the current
			// dimension to the dot product.
			r += dx * dir_i 
		}

		// divide dot product by squared length of directional vector to obtain
		// cosine of the angle between the copy attempt direction and the
		// direction to the attraction point. This cosine is 1 if they are
		// perfectly aligned, 0 if they are perpendicular, and negative
		// if the directions are opposite. Since we want to reward copy attempts
		// in the right direction, deltaH is the negative of this (and
		// multiplied by the lambda weight factor).
		return - r * l / Math.sqrt( ldir )
	}
}

export default AttractionPointConstraint