Skip to content

Commit

Permalink
further updates to doc. additional test for the non diagonal model ba…
Browse files Browse the repository at this point in the history
…sis. slight refactor to RealignExtendedBasis
  • Loading branch information
JonathanAellen committed Jun 24, 2024
1 parent 51c1203 commit db9eced
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -372,12 +372,15 @@ class DiscreteLowRankGaussianProcess[D: NDSpace, DDomain[DD] <: DiscreteDomain[D
* True if the extended basis should be included. By default this uses a rotation extension. False makes the
* realignment only over translation. Translational alignment can be done exactly.
* @param diagonalize
* True if a diagonal basis should be returned. In general, it is strongly recommended to use a diagonal basis. This
* option can be set to false if the resulting model is only sampled from (for example [[sample]] or [[instance]]).
* This makes the same coefficient lead to very similar shapes in the pre- and after realignment model (or exactly
* the same shapes if withExtendedBasis = false).
* True if a diagonal basis should be returned. In general, it is strongly recommended to use a orthonormal basis -
* here referred to as diagonal. This is not increase complexity and is a more intuitive formulation of the model.
* If internal fields are accessed diagonalize should be set to true. This option can be set to false to make the
* same coefficient lead to very similar shapes in the pre- and after realignment model (or exactly the same shapes
* if withExtendedBasis = false).
* @return
* The resulting [[DiscreteLowRankGaussianProcess]] aligned on the provided instances of [[PointId]]
* The resulting [[DiscreteLowRankGaussianProcess]] aligned on the provided instances of [[PointId]]. If
* withExtendedBasis = false then the original and the returned model can produce the same mesh with different
* translations. That means the shape spaces are the same but the fieldsa are translated.
*/
def realign(ids: IndexedSeq[PointId], withExtendedBasis: Boolean = true, diagonalize: Boolean = true)(using
vectorizer: Vectorizer[Value],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,34 @@ import breeze.linalg.DenseMatrix
import scalismo.common.DiscreteDomain
import scalismo.geometry.*

/**
* ideally used to represent linear effects that should be normalized between the training data. The realignment process
* then builds a linear projection matrix that is applied to an existing model. the space of the effects that should be
* normalized needs to be spanned by the returned matrix
*/
trait RealignExtendedBasis[D, Value]:

/**
* whether or not the default translation basis should also be used. that means false does not perform a translation
* realignment.
*/
def useTranslation: Boolean

/**
* basis to span the kernel of the projection. as example, a translation alignment here could span that space with
* constant vectors for each cardinal direction.
*/
def getBasis[DDomain[DD] <: DiscreteDomain[DD]](model: DiscreteLowRankGaussianProcess[D, DDomain, Value],
center: Point[D]
): DenseMatrix[Double]
def centeredP[DDomain[DD] <: DiscreteDomain[DD]](domain: DDomain[D], center: Point[D]): DenseMatrix[Double] = {

/**
* includes the additional default rotation centerpoint implementation which is useful to calculate the rotation basis.
*/
trait RealignExtendedBasisRotation[D, Value] extends RealignExtendedBasis[D, Value]:
def centeredP[D: NDSpace, DDomain[DD] <: DiscreteDomain[DD]](domain: DDomain[D],
center: Point[D]
): DenseMatrix[Double] = {
// build centered data matrix
val x = DenseMatrix.zeros[Double](center.dimensionality, domain.pointSet.numberOfPoints)
val c = center.toBreezeVector
Expand All @@ -23,7 +44,7 @@ object RealignExtendedBasis:
* returns a projection basis for rotation. that is the tangential speed for the rotations around the three cardinal
* directions.
*/
given realignBasis3D: RealignExtendedBasis[_3D, EuclideanVector[_3D]] with
given realignBasis3D: RealignExtendedBasisRotation[_3D, EuclideanVector[_3D]] with
def useTranslation: Boolean = true
def getBasis[DDomain[DD] <: DiscreteDomain[DD]](
model: DiscreteLowRankGaussianProcess[_3D, DDomain, EuclideanVector[_3D]],
Expand All @@ -50,7 +71,7 @@ object RealignExtendedBasis:
/**
* returns a projection basis for rotation. that is the tangential speed for the single 2d rotation.
*/
given realignBasis2D: RealignExtendedBasis[_2D, EuclideanVector[_2D]] with
given realignBasis2D: RealignExtendedBasisRotation[_2D, EuclideanVector[_2D]] with
def useTranslation: Boolean = true
def getBasis[DDomain[DD] <: DiscreteDomain[DD]](
model: DiscreteLowRankGaussianProcess[_2D, DDomain, EuclideanVector[_2D]],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,11 +508,11 @@ class GaussianProcessTests extends ScalismoTestSuite {

object Fixture {
val domain = BoxDomain((-5.0, -5.0, -5.0), (5.0, 5.0, 5.0))
val sampler = UniformSampler(domain, 6 * 6 * 6)
val sampler = UniformSampler(domain, 5 * 5 * 5)
val mean = Field[_3D, EuclideanVector[_3D]](EuclideanSpace[_3D], _ => EuclideanVector(0.0, 0.0, 0.0))
val gp = GaussianProcess(mean, DiagonalKernel(GaussianKernel[_3D](5), 3))

val lowRankGp = LowRankGaussianProcess.approximateGPNystrom(gp, sampler, 200)
val lowRankGp = LowRankGaussianProcess.approximateGPNystrom(gp, sampler, 120)

val discretizationPoints = sampler.sample().map(_._1)
val discreteLowRankGp = DiscreteLowRankGaussianProcess(UnstructuredPointsDomain(discretizationPoints), lowRankGp)
Expand Down Expand Up @@ -771,6 +771,51 @@ class GaussianProcessTests extends ScalismoTestSuite {
})
res(1) shouldBe <(res(0) * 0.6)
}

it("the non diagonalized model should be equivalent to the diagonalized one") {
val f = Fixture
val dgp = f.discreteLowRankGp

val ids = {
val sorted = dgp.mean.pointsWithIds.toIndexedSeq.sortBy(t => t._1.toArray.sum)
sorted.take(10).map(_._2)
}
val coef = (0 until 5).map(_ => this.random.scalaRandom.nextInt(100))
val adDgp = dgp.realign(ids, withExtendedBasis = false)
val aDgp = dgp.realign(ids, withExtendedBasis = false, diagonalize = false)

// true variance is not the same as naive reading of variance field
val varad = adDgp.variance.data.sum
val vara = aDgp.variance.data
.zip(breeze.linalg.norm(aDgp.basisMatrix, breeze.linalg.Axis._0).t.data)
.map(t => t._1 * (t._2 * t._2))
.sum
varad shouldBe vara +- 1e-5

// projection works as it relies on underlying regression calculations
val sample = adDgp.sample()
val proj = aDgp.project(sample)
val dif = sample.data.zip(proj.data).map(t => (t._1 - t._2).norm).sum
dif shouldBe 0.0 +- 1e-1

// marginal likelihood works
val marglad = adDgp.marginalLikelihood(f.trainingDataDiscreteGP)
val margla = aDgp.marginalLikelihood(f.trainingDataDiscreteGP)
marglad shouldBe margla +- 1e-5

// regression mean and variance are also the same
val td = f.trainingDataDiscreteGP.map(t => (t._1, EuclideanVector3D.ones, t._3))
val regmeanad = adDgp.posterior(td)
val regmeana = aDgp.posterior(td)
val difmeans = regmeanad.mean.data.zip(regmeana.mean.data).map(t => (t._1 - t._2).norm).sum
difmeans shouldBe 0.0 +- 1e-1
val corvarad = regmeanad.variance.data.sum
val corvara = regmeana.variance.data
.zip(breeze.linalg.norm(regmeana.basisMatrix, breeze.linalg.Axis._0).t.data)
.map(t => t._1 * (t._2 * t._2))
.sum
corvarad shouldBe corvara +- 1e-5
}
}
}

Expand Down

0 comments on commit db9eced

Please sign in to comment.