R/polarzonoid.R

Defines functions support boundarygradient boundarynormal stratumdimension boundaryfunction boundaryparts arcsfromsphere arcsfromboundary dumpalong boundaryfromsphere spherefromboundary spherefromarcs_plus spherefromarcs boundarynames supportingnormal boundaryfromarcs

Documented in arcsfromboundary arcsfromsphere boundaryfromarcs boundaryfromsphere spherefromarcs spherefromarcs_plus spherefromboundary support

p.arcmatcolnames    = c( 'center', 'length' )


#   arcmat  an Nx2 matrix, with data on N disjoint arcs
#           in each row, the first number is the center of the arc
#           the second number is the length of the arc
#
#   m       dimension of space containing zonoid, it must be odd and >= 3
#
#   gapmin  for verifying that arcmat[] has disjoint arcs
#           the default gapmin=0 allows abutting arcs
#           if gapmin<0 some overlap is allowed.  If gapmin == -Inf, then test is skipped
#
#   returns a list with these items:
#       *) arcmat   the given arcs
#       *) point    the corresponding point on the polar zonoid, defined by trig polynomials
#       *) normal   outward normal of a supporting plane at point


boundaryfromarcs <- function( arcmat, n=NULL, gapmin=0 )
    {
    arcmat   = prepareNxM( arcmat, 2 )
    if( is.null(arcmat) )    return(NULL)

    #   check individual lengths
    neg = (arcmat[ ,2] < 0)
    if( any(neg) )
        {
        log_level( ERROR, "%d of %d arcs have negative length.", sum(neg), length(neg) )
        return(NULL)
        }

    if( gapmin != -Inf )
        {
        #   validation check
        gm  = gapminimum( arcmat )

        if( gm < gapmin )
            {
            log_level( ERROR, "Arcs are not disjoint. minimum gap = %g < %g.", gm, gapmin )
            return(NULL)
            }
        }

    ltotal  = sum( arcmat[ ,2] )

    improper    = ltotal==0  ||  ltotal==2*pi

    if( improper )
        nmin    = 0L
    else
        nmin    = nrow(arcmat)

    if( is.null(n) )    n   = nmin

    #   check that n is large enough
    if( n < nmin )
        {
        log_level( ERROR, "Argument n=%d is invalid, because it is < %d.", n, nmin )
        return(NULL)
        }


    m   = 2L*n + 1L

    if( improper )
        {
        #   return the 'south pole' or 'north pole'

        out     = numeric(m)
        out[m]  = ltotal

        #out$normal  = numeric(m)
        #out$normal[m]   = ltotal/pi - 1

        return(out)
        }

    #   since improper is FALSE,  n > 0

    point   = numeric(0)

    theta   = endpointsfromarcs( arcmat )

    for( j in 1:n )
        {
        cossin  = numeric(2)

        for( k in 1:nrow(arcmat) )
            {
            thetalo = j*( theta[k,1] )
            thetahi = j*( theta[k,2] )

            xypair  =  c( sinmy(thetahi) - sinmy(thetalo), -cosmy(thetahi) + cosmy(thetalo) ) / j

            cossin  = cossin + xypair
            }

        point = c( point, cossin )
        }

    #   append the total length
    point[m]  = ltotal

    names(point) = boundarynames(m)

    out = point

    if( FALSE )
    {
    if( m <= 7 )
        {
        #   use the known gradient of the known boundary function
        #   this is more reliable
        out$normal  = boundarynormal( point )
        }
    else if( 2*length(theta)+1 == length(point) )
        {
        #   use the jacobian at theta, and then SVD
        #   this can fail if the rcond is too small
        out$normal  = supportingnormal( theta, point )
        }
    else
        #   cannot be determined
        out$normal  = NA_real_
    }


    #   out$bvalue  = boundaryfunction(point)

    return( out )
    }



#   theta   n x 2 matrix with thetamin and thetamax in the rows
#           these are the endpoints of the arcs that map to the boundary point
#
#   point   point on the boundary of the zonoid
#           this is only used to ensure that the normal is outward-pointing
#
#   tol     tolerance for smallest singular value
#
#   returns 2*n+1 normal to the zonoid at point, unitized
#
#   if the boundary is not differentiable at the corresponding point on the boundary
#   the function returns NA_real_

supportingnormal <- function( theta, point, tol=5.e-14 )
    {
    n   = nrow(theta)

    #   the mapping is from 2*n parameters to (2*n + 1) parameters

    #   make jacobian matrix that is 2*n+1  x  2*n
    m   = 2L*n + 1L

    if( length(point) != m )
        {
        log_level( ERROR, "Internal error.  length(point) = %d  !=  %d = 2*nrow(theta)+1.", length(point), m )
        return(NULL)
        }

    jac = array( 0, dim=c(m,2*n) )

    for( i in 1:n )
        {
        for( k in 1:n )
            {
            thetalo = i * theta[k,1]
            thetahi = i * theta[k,2]

            jac[ 2*i-1, 2*k-1 ] = -cosmy(thetalo)
            jac[ 2*i-1, 2*k ]   =  cosmy(thetahi)
            jac[ 2*i,   2*k-1 ] = -sinmy(thetalo)
            jac[ 2*i,   2*k ]   =  sinmy(thetahi)
            }
        }

    #   now the last row
    for( k in 1:n )
        {
        jac[ m, 2*k-1]  = -1
        jac[ m, 2*k  ]  = +1
        }

    #print( jac )

    #   use svd() to find a unit vector that is orthogonal to all the columns
    res = base::svd( jac, nu=m, nv=0 )  #; print( res )

    rcond   = res$d[m-1] / res$d[1]

    if( rcond <= tol )
        {
        log_level( WARN, "rcond = %g <= %g.  Supporting normal is set to NA.", rcond, tol )
        return(NA_real_)
        }

    normal  = res$u[ ,m]

    testdot = sum( normal * point ) - normal[m]*pi
    if( testdot < 0 )
        #   change from inward to outward
        normal  = -normal

    names(normal)   =   names(point)

    return( normal )
    }



#   m   an odd number

#   returns a character vector of length m

boundarynames <- function( m )
    {
    n   = (m-1)/2   # of arcs

    out = character(0)

    for( i in seq_len(n) )
        out = c( out, paste( c('x','y'), i, sep='' ) )

    out = c( out, 'L' )

    return( out )
    }



#   arcmat  an Nx2 matrix, with data on N arcs
#           in each row, the first number is the center of the arc
#           the second number is the length of the arc
#
#   n       the given set of arcs is taken to be in \eqn{A_n}
#
#   returns corresponding point on the polar zonoid, then projected
#   onto unit sphere centered at the center of the zonoid = (0,0,0,0, ... , pi)

spherefromarcs <- function( arcmat, n=NULL, gapmin=0 )
    {
    p   = boundaryfromarcs( arcmat, n=n, gapmin=gapmin )

    if( is.null(p) )  return(NULL)

    p   = as.numeric(p)

    return( spherefromboundary(p) )
    }



#   arcmat  an Nx2 matrix, with data on n0 arcs.  n0 = nrow(arcmat)
#           in each row, the first number is the center of the arc
#           the second number is the length of the arc
#
#   n       the given set of arcs is taken to be in \eqn{A_n}
#
#   returns a list with these items:
#       u       the point on the sphere that arcmat maps to - a unit vector
#       tangent 2n+1  x  2n0 matrix.  The jacobian of the map at arcmat.
#               the 2*n0 columns are tangent to the stratum A_n0
#       normal  2n+1 x 2*(n-n0) matrix.  The columns are an orthonormal basis
#               for the normal space to the stratum A_n0 inside A_n
#
#   if necessary, one of the normals is flipped so that
#       det( cbind(u,tangent,normal) ) > 0

spherefromarcs_plus <- function( arcmat, n=NULL, gapmin=5.e-10 )
    {
    arcmat   = prepareNxM( arcmat, 2 )
    if( is.null(arcmat) )    return(NULL)

    if( gapmin <= 0 )
        {
        log_level( WARN, "gapmin = %g <= 0 is invalid.", gapmin )
        return(NULL)
        }

    p   = boundaryfromarcs( arcmat, n=n, gapmin=gapmin )

    if( is.null(p) )  return(NULL)

    p   = as.numeric(p)

    u   = spherefromboundary(p)

    n0    = nrow(arcmat)

    if( is.null(n) )    n = n0

    #   make jacobian matrix that is 2*n+1  x  2*n0
    theta   = endpointsfromarcs( arcmat )

    m   = 2L*n + 1L

    jac = array( 0, dim=c(m,2*n0) )

    for( i in 1:n )
        {
        for( k in 1:n0 )
            {
            thetalo = i * theta[k,1]
            thetahi = i * theta[k,2]

            jac[ 2*i-1, 2*k-1 ] = -cosmy(thetalo)
            jac[ 2*i-1, 2*k ]   =  cosmy(thetahi)
            jac[ 2*i,   2*k-1 ] = -sinmy(thetalo)
            jac[ 2*i,   2*k ]   =  sinmy(thetahi)
            }
        }

    #   now the last row
    for( k in 1:n0 )
        {
        jac[ m, 2*k-1]  = -1
        jac[ m, 2*k  ]  = +1
        }

    #   the columns of jac are tangent to the boundary of the zonoid
    #   to get the tangent vectors to the sphere,
    #   project columns of jac onto the plane normal to u

    ujac    = as.numeric( u %*% jac )

    tangent = jac - u %o% ujac

    if( n == n0 )
        {
        #   set normal to a matrix with 0 columns
        out = list( u=u, tangent=tangent, normal=matrix(0,nrow=m,ncol=0 ) )
        return( out )
        }

    mat = cbind( u, tangent )   # mat has 2*n0+1 columns

    #   use svd() to find 2*(n-n0) unit vectors that are orthogonal to all the columns of mat
    res = base::svd( mat, nu=m, nv=0 ) # ; print( res )

    rcond   = res$d[ length(res$d) ] / res$d[1]

    tol = 1.e-14

    if( tol < rcond )
        {
        normal  = res$u[ , (m - 2*(n-n0) +1):m ]

        #   check the orientation and make it + if necessary
        mat = cbind( mat, normal )

        #   mat[,] is now square, m x m
        if( base::determinant( mat )$sign < 0 )
            #   reverse the sign of last normal vector
            normal[ ,ncol(normal)]  = -normal[ ,ncol(normal)]
        }
    else
        {
        normal  = NA_real_
        log_level( WARN, "rcond = %g <= %g.  normal[,] is set to NA.", rcond, tol )
        }

    out = list( u=u, tangent=tangent, normal=normal )

    return( out )
    }




#   p   a point known to be on the boundary of the zonoid
#       p should come from boundaryfromsphere()  OR   boundaryfromarcs()
#       if p has even dimension, it is interpreted as being on the equator of the zonoid of one higher dimension

spherefromboundary <- function( p )
    {
    m   = length(p)

    if( (m %% 2) == 1 )
        {
        #   odd is the usual cases
        x       = p
        x[m]    = x[m] - pi
        }
    else
        {
        #   if even, interpret p as on the equator, just append 0
        x   = c( p, 0 )
        }

    x   = as.numeric( x )

    #   x now has odd length
    return( unitize( x ) )
    }



#   x   a non-zero vector.  If even-dimensional, then 0 is appended to make it odd-dimensional
#       x is usually on the sphere, but if not it is re-unitized to make it so

boundaryfromsphere <- function( x )
    {
    u   = unitize(x)

    if( any( is.na(u) ) )
        {
        log_level( ERROR, "argument x is 0, which is invalid." )
        return(NULL)
        }

    if( (length(u) %% 2L) == 0L )
        #   x is even-dimensional, so interpret as being on equator.  Append a 0.
        u   = c( u, 0 )

    #   u is now odd-dimensional
    m   = length(u)

    n   = (m-1L) / 2L       # n is the number of arcs

    if( 3 < n )
        {
        log_level( ERROR, "The number of arcs is n=%d, but in this package version it must be 1, 2, or 3.", n )
        return(NULL)
        }

    center  = c( numeric(m-1), pi )

    if( u[m] == 0  &&  n < 3 )
        {
        #   in this special case, there is a closed-form expression for the root
        if( n == 1 )
            # only 1 arc, n=1 and m=3
            root   = 2
        else
            {
            #   2 arcs, n=2, and m=5
            beta    = min( sum(u[1:2]^2), 1 )   # do not allow even a tiny bit more than 1

            root   = 4 / (sqrt(1 + 3*beta) + sqrt(1 - beta))
            }

        out = center + root*u

        names(out)  = boundarynames(m)
        
        log_level( INFO, "computed root=%g using closed form expression.  f(root) = %g.",
                                root, boundaryfunction(out) )

        return( out )
        }


    #   the usual case, use uniroot()
    myfun <- function( s )  { boundaryfunction( center + s*u ) }

    #   the root must be in the interval [rootmin,rootmax]
    if( n == 1 )
        rootmin = 2 - 0.1   # expand a little
    else
        rootmin = 1

    rootmax = 2.2 # pi

    #   expand the interval just a bit
    #  rootmin = rootmin - 0.1

    # cat( "myfun(endpoints) = ", myfun(rootmin), "  ", myfun(rootmax), '\n' )


    #   in the next call, tol is much smaller than the default
    res = try( stats::uniroot( myfun, c(rootmin,rootmax), tol=.Machine$double.eps^0.75, extendInt="upX" ), silent=FALSE )   #; print( str(res) )

    if( ! inherits(res,"try-error" ) )    # class(res) == "try-error" )
        {
        #   success
        root   = res$root

        log_level( INFO, "computed transverse root = %g using stats::uniroot().  f(root) = %g.",
                                root, res$f.root )
        }
    else
        {
        #   failure
        root    = NA_real_

        log_level( WARN, "stats::uniroot() failed. myfun() at endpoints.  myfun(%g)=%g   myfun(%g)=%g.",
                            rootmin, myfun(rootmin), rootmax, myfun(rootmax) )

        if( FALSE )
            {
            # print( dumpalong( seq( rootmin, rootmax, len=101 ), u ) )
            }

        #  cat( 'stats::uniroot()  res = ', utils::str(res), '\n', file=stderr() )
        return( NULL )
        }

    if( is.finite(root)  &&  root <= pi )
        {
        #   the root is probably valid
        out = center + root*u

        names(out)  = boundarynames(m)

        return( out )
        }


    #   the computed transverse root is probably bogus
    #   the true root is probably non-transverse,  where the x-axis is tangent to the curve
    #   find the maximum on the interval [rootmin,pi]
    res = stats::optimize( myfun, interval=c(rootmin,pi), maximum=TRUE, tol=.Machine$double.eps^0.50 )

    tol = 5.e-12
    
    if( -tol < res$objective )
        {
        #  success
        root    = res$maximum

        log_level( INFO, "computed non-transverse root = %g using stats::optimize().    f(root) = %g > %g.",
                                    root, res$objective, -tol )

        out = center + root*u

        names(out)  = boundarynames(m)
        
        return( out )
        }

        
    #   failure
    log_level( ERROR, "stats::optimize() failed to compute root.  argmax = %g and f(argmax) = %g >= %g.",
                                    res$maximum, res$objective, -tol )

    return( NULL )
    }


dumpalong <- function( svec, u )
    {
    m   = length(u)

    center  = c( numeric(m-1), pi )

    count   = length(svec)

    lhs     = complex(count)
    rhs     = numeric(count)
    value   = numeric(count)

    for( k in 1:count )
        {
        x   = center + svec[k]*u
        z   = complexfromrealvector( x )
        L   = x[ length(x) ]

        res = boundaryparts( z, L )
        lhs[k]  = res$lhs
        rhs[k]  = res$rhs
        value[k] = abs(res$lhs) - res$rhs
        }

    out = data.frame( svec=svec )
    out$lhs = lhs
    out$rhs = rhs
    out$value   = value

    return(out)
    }



#   p   a non-zero vector, on the boundary of the zonoid, with length 2 to 7
#       p should come from boundaryfromarcs() or boundaryfromsphere().
#       verification of this boundary property is minimal.
#       if the length is even, a pi is appended to  put the point on the "equator"
#       so the number of arcs must be 0, 1, 2, or 3
#
#   tol if the distance from p to a sub-stratum of the boundary is less than tol
#       then take p to actually *be* in this substratum and compute the arcs from there
#
#   returns a matrix arcmat.  Or NULL in case of error.

arcsfromboundary <- function( p, tol=5.e-9 )
    {
    if( (length(p) %% 2) == 0 )   p = c( p, pi )    #  put the point on the "equator"

    m   = length(p)

    n   = (m-1L) / 2L       # n is the number of arcs

    ok  = n %in% (0:3)
    if( ! ok )
        {
        log_level( ERROR, "The number of arcs is %d, which is invalid in this version of the package.", n )
        return(NULL)
        }

    L   = p[m]

    if( FALSE  &&  m == 5 )
        {
        #   test whether the 2 arcs compute arcs will be abutting
        test    = sqrt( sum( p[1:2]^2 ) ) - crd(p[5])

        if( abs(test) <= tol )
            {
            #   just return a single arc, ignore the double angle parts 3 & 4
            p   = p[ c(1,2,5) ]
            m   = length(p)
            n   = (m-1L) / 2L
            }
        }

    normal  = boundarynormal( p, tol=tol )

    if( is.null(normal)  ||  is.na( normal[1] ) )
        {
        log_level( WARN, "The boundary normal cannot be computed.\n" )
        return(NULL)
        }

    #  convert normal to coefficients of a trig polynomial
    ablist  = trigpolyfromvector( normal )

    #   the length of theta is always even, and possibly 0
    theta   = trigpolyroot( ablist ) #; print( theta )

    out = arcsfromendpointsandpoly( theta, ablist )

    #  cat( "normal = ", normal, "   theta = ", theta, '\n' )

    return( out )
    }



#   u   a non-zero vector, with length 2 or more
#   if the length is even, a 0 is appended to make the length odd
#   typically it is a point in the unit sphere, but if not it is unitized automatically
#
#   returns a matrix arcmat, to invert spherefromarcs()
#   the number of arcs is floor(length(u)/2) or less.
#   if u is the "north pole", a single improper arc of length 2*pi is returned


arcsfromsphere <- function( u )
    {
    p   = boundaryfromsphere( u )

    if( is.null(p) )    return(NULL)

    arcmat  = arcsfromboundary( p )

    return( arcmat )
    }



    
    
#   z   complex vector of length n = # of arcs
#   L   total length of arcs
#
#   return list with:
#       lhs     complex number
#       rhs     real number

boundaryparts <- function( z, L )
    {
    n   = length(z)

    if( n == 1 )
        {
        lhs = z[1]
        rhs = 2*sinmy(L/2)
        }
    else if( n == 2 )
        {
        #   lhs is complex
        lhs = 2*sinmy(L/2)*z[2] - cosmy(L/2)*z[1]^2

        # rhs is real
        # if x is *on* the boundary, rhs>=0 and is 0 iff 2 arcs degenerate to 1
        rhs = 4*sinmy(L/2)^2 - abs2(z[1])

        #x1  = x[1]
        #y1  = x[2]
        #x2  = x[3]
        #y2  = x[4]

        #lhs_old = sqrt( (2*sinmy(L/2)*x2 - cosmy(L/2) * (x1^2 - y1^2))^2  +   (2*sinmy(L/2)*y2 - cosmy(L/2) * 2*x1*y1)^2  )
        #rhs_old = 4*sinmy(L/2)^2 - (x1^2 + y1^2)

        #old = lhs_old - max( rhs_old, 0 )
        }
    else if( n == 3 )
        {
        z1  = z[1]
        z2  = z[2]
        z3  = z[3]

        sine    = sinmy(L/2)
        cosine  = cosmy(L/2)
        z1abs2  = abs2(z1)

        #   lhs and rhs were computed with help from Macaulay2

        lhs = 12*(4*sine^2 - z1abs2)*z3  +  (8*cosine^2 - z1abs2 + 4)*z1^3  -  48*sine*cosine*z1*z2  +  12*z2^2*Conj(z1)    # Conj(z2) missing

        #   the rhs is actually real
        #   if x is *on* the boundary, rhs>=0 and is 0 iff 3 arcs degenerate to 2
        rhs = 6*sine*( z1abs2^2  -  8*z1abs2  -  4*abs2(z2) )  +  12*cosine*iprod( z1^2, z2 )  +  96*sine^3         # z3 is missing
        }

    out = list( lhs=lhs, rhs=rhs )

    return( out )
    }



#   x   a point in R^(2*n+1)
#       m=length(x) must be positive and odd

boundaryfunction <- function( x )
    {
    m   = length(x)

    ok  = (1 <= m)  &&  ((m %% 2L)==1L)

    if( ! ok )
        {
        log_level( ERROR, "length(x) = %d is invalid.", m )
        return( NA_real_ )
        }

    #   n is the number of arcs
    n   = as.integer( (m-1)/2 )

    Lgiven  = x[m]

    #   clamp Lgiven to [0,2*pi], and use L in calculations
    L   = min( max( Lgiven, 0 ), 2*pi )


    if( FALSE  &&  L == pi )
        {
        #   a big simplification
        even    = 2L *( 1:n )
        odd     = even - 1L

        zmod2   = as.numeric( x[odd]^2 + x[even]^2 )

        if( n == 3 )
            {
            zmod = sqrt( zmod2 )

            out = (1/4)*zmod[1]^3  +  (1/2)*zmod[2]^2  +  zmod[3] - 2

            return( out )
            }

        out = sqrt(zmod2[n]) - 2

        if( 2 <= n )
            {
            for( k in (n-1):1 )
                out = zmod2[k]^((n+1-k)/2) + 2*out
            }

        return( out )
        }

    if( n == 0 )
        {
        #   then m=length(x) must be 1
        #   the zonoid is the interval [0,2*pi]
        out = x*(x - 2*pi)/(2*pi)
        }
    else if( n == 1 )
        {
        L   = Lgiven

        out = as.numeric( sqrt( sum( x[1:2]^2 ) ) )

        if( L <= 0 )
            out = out - L
        else if( L <= 2*pi )
            out = out - crd(L)
        else
            out = out + (L - 2*pi)
        }
    else if( n %in% 2:3 )
        {
        z   = complexfromrealvector( x )

        res = boundaryparts( z, L )

        out = abs(res$lhs) - res$rhs    # max(rhs,0)

        if( L != Lgiven )
            #   Lgiven is outside the prescribed interval, so add penalty
            out = out + (Lgiven - L)^2

        names(out)  = NULL
        }
    else
        {
        log_level( WARN, "The number of arcs n=%d is too large. n must be 0,1,2, or 3.  Returning NA.", n )
        return( NA_real_ )
        }

    return( out )
    }

#   x   a point in R^(2n+1) where n is 0, 1 or 2 or 3
#       x is also assumed to be on the boundary of the polar zonoid, but this is not verified.
#       x should really come from boundaryfromarcs() or boundaryfromsphere()
#
#   tol if the distance from x to a sub-stratum of the boundary is less than tol
#       then take x to actually *be* in this substratum and return the dimension of that substratum
#
#   returns the dimension of the stratum that contains x.
#   this is 2*|arcs|

stratumdimension <- function( x, tol=5.e-9 )
    {
    m   = length(x)

    ok  = (1 <= m) && (m <= 7)  &&  ((m %% 2L)==1L)

    if( ! ok )
        {
        log_level( ERROR, "length(x) = %d is invalid.", m )
        return( NA_integer_ )
        }

    if( m == 1 )    return( 0L )    # trivial case, x must be 0 or 2*pi.  x is one of the "poles".

    z   = complexfromrealvector( x )

    L   = x[m]      # sum of all arc lengths

    #   n is the number of arcs
    n   = length(z)

    for( k in 1:n )
        {
        res = boundaryparts( z[1:k], L )

        #cat( "k=", k, "  rhs=", res$rhs, '\n' )

        #   the rhs is the "distance" in the stratification
        if( res$rhs <= tol )    return( 2L * (k - 1L) )
        }

    return( 2L * n )
    }





#   x   a point in R^(2n+1) where n is 0, 1 or 2 or 3
#       x is also assumed to be on the boundary of the polar zonoid, but this is not verified.
#       x should really come from boundaryfromarcs() or boundaryfromsphere()
#       A fundamental property of convex bodies is that every boundary point
#       has a supporting normal
#
#   tol if the distance from x to a sub-stratum of the boundary is less than tol
#       then take x to actually *be* in this substratum and compute the normal there
#
#   returns a supporting normal to boundary at x, unitized.
#   in the generic case, this normal is the gradient of the boundaryfunction() at x, and then unitized

boundarynormal <- function( x, tol=5.e-9 )
    {
    out = boundarygradient( x, tol )

    if( is.na(out[1]) ) return( out )

    return( unitize(out) )
    }


boundarygradient <- function( x, tol=5.e-9 )
    {
    m   = length(x)

    ok  = (1 <= m) && (m <= 7)  &&  ((m %% 2L)==1L)

    if( ! ok )
        {
        #   log_level( ERROR, "length(x) = %d is invalid.", m )
        return( rep(NA_real_,m) )
        }

    L   = x[m]      # sum of all arc lengths

    #n   = as.integer( (m-1)/2 )

    #   test for being close to a substratum
    #   n is the number of arcs
    n   = stratumdimension( x, tol=tol ) / 2L

    if( is.na(n) )  return( rep(NA_real_,m) )

    out = numeric(m)

    if( n == 0 )
        {
        #   L is either 0 or 2*pi
        out[m]  = L/pi - 1  # -1 or +1
        }
    else if( n == 1 )
        {
        #   the gradient here is simple
        out[1:2] = unitize( x[1:2] )

        out[m]  = -cosmy( L/2 )
        }
    else if( n == 2 )
        {
        #---  using complex numbers
        z       = complexfromrealvector( x[1:4] )       # z[] has length 2
        sine    = sinmy(L/2)
        cosine  = cosmy(L/2)

        #   gradient of the LHS
        lhs     = cosine*z[1]^2  -  2*sine*z[2]
        abslhs  = abs(lhs)
        if( abslhs == 0 )
            {
            # this means that 2 arcs are abutting.  There is a crease on the boundary
            # this should not happen if tol > 0,
            # since 2 abutting arcs should have forced n=1 in the substrata test
            log_level( WARN, "LHS is 0, so given point x = %s is at a crease.", paste(x[1:4],collapse=',') )            
            return( rep(NA_real_,m) )
            }

        zL  = -sine*z[1]^2/2 - cosine*z[2]

        jac2x5  = cbind( real2x2Nfromcomplex( c( 2*cosine*z[1], -2*sine ) ), realfromcomplexvector(zL) )  #; print( jac2x5 )

        jac1x2  = realfromcomplexvector(lhs)/abslhs     #; print( jac1x2 )

        gradlhs = as.numeric(  jac1x2  %*%  jac2x5  )

        #   gradient of the RHS
        gradrhs = c( realfromcomplexvector( c( -2*z[1], 0 ) ), 2*sinmy(L) )     # length is 5

        grad    = gradlhs - gradrhs

        out = grad

        if( FALSE )
            {
            #   using real numbers
            x1  = x[1]
            y1  = x[2]
            x2  = x[3]
            y2  = x[4]

            C   = 2*sinmy(L/2)*x2 - cosmy(L/2)*(x1^2 - y1^2)
            D   = 2*sinmy(L/2)*y2 - cosmy(L/2)*(2*x1*y1)

            G   = sqrt( C^2 + D^2 )
            if( G == 0 )
                {
                # this means that 2 arcs are abutting.  There is a crease on the boundary
                # this should not happen if tol > 0,
                # since 2 abutting arcs should have forced n=1 in the substrata test
                log_level( WARN, "LHS is 0, so given point x = %s is at a crease.", paste(x,collapse=',') )
                return( rep(NA_real_,m) )
                }

            out[1]  = (2*C*2*x1  +  2*D*2*y1) * (-cosmy(L/2))

            out[2]  = (-2*C*2*y1  +  2*D*2*x1) * (-cosmy(L/2))

            out[3]  = 2*C*2*sinmy(L/2)

            out[4]  = 2*D*2*sinmy(L/2)

            out[5]  = (2*x2^2 + 2*y2^2 - 0.5*(x1^2 + y1^2)^2)*sinmy(L)  -  2*(x2*(x1^2 - y1^2) + y2*2*x1*y1)*cosmy(L)

            #   add correction from chain rule
            out = (0.5 * 1/G) * out

            #   and subtract the gradient of the second term
            out[1]  = out[1] + 2*x1
            out[2]  = out[2] + 2*y1
            out[5]  = out[5] - 2*sinmy(L)

            #   compare out and grad
            #cat( "out = ", out, '\n' )
            #cat( "grad = ", grad, '\n' )
            #cat( "delta = ", max( abs(out-grad) ), '\n' )
            }
        }
    else if( n == 3 )
        {
        #---  using complex numbers
        z       = complexfromrealvector( x[1:6] )       # z[] has length 3

        lhs     = boundaryparts( z, L )$lhs
        abslhs  = abs(lhs)
        if( abslhs == 0 )
            {
            # this means that 2 arcs are abutting.  There is a crease on the boundary
            # this should not happen if tol > 0,
            # since 2 abutting arcs should have forced n=1 in the substrata test
            log_level( WARN, "LHS is 0, so given point x = %s is at a crease.", paste(x[1:6],collapse=',') )
            return( rep(NA_real_,m) )
            }
            
        sine    = sinmy(L/2)
        cosine  = cosmy(L/2)
        abs2z1  = abs2( z[1] )
        xy1     = realfromcomplexvector( z[1] )

        #   gradient of the LHS
        pz1 =  8*cosine^2*3*z[1]^2  -  abs2z1*3*z[1]^2  +  12*z[1]^2  - 48*sine*cosine*z[2]   # more to be added later

        pz2 = -48*sine*cosine*z[1]  +  24*Conj(z[1])*z[2]

        pz3 = 48*sine^2 - 12*abs2z1

        pL  = 48*sine*cosine*z[3]  -  8*sine*cosine*z[1]^3  - 24*(cosine^2 - sine^2)*z[1]*z[2]

        #   start with just 2x2
        jac2x7  = real2x2Nfromcomplex( pz1 )

        jac2x7  = jac2x7  -  real2x2Nfromcomplex( 12*z[3] + z[1]^3 )  %*%  rbind( 2*xy1, 0 )

        jac2x7  = jac2x7  +  real2x2Nfromcomplex( 12*z[2]^2 )  %*%  matrix( c(1,0,0,-1), 2, 2 )

        #   now add 5 more columns
        jac2x7  = cbind( jac2x7, real2x2Nfromcomplex( c(pz2,pz3) ), realfromcomplexvector(pL) )

        jac1x2  = realfromcomplexvector(lhs)/abslhs     #; print( jac1x2 )

        gradlhs = as.numeric(  jac1x2  %*%  jac2x7  )           # length is 7


        #   gradient of the RHS
        zgrad   = c( -48*sine*2*z[1] + 6*sine*4*abs2z1*z[1] + 12*cosine*4*Conj(z[1])*z[2], -24*sine*2*z[2] + 12*cosine*2*z[1]^2, 0 )

        partL   = 144*sine^2*cosine  -  24*cosine*abs2z1  +  3*cosine*abs2z1^2  -  12*cosine*abs2(z[2])  -  6*sine*iprod(z[1]^2,z[2])

        gradrhs = c( realfromcomplexvector(zgrad), partL )      # length is 7

        grad    = gradlhs - gradrhs

        out = grad
        }

    return(out)
    }





#
#   direction   RxM matrix, with the R directions in the rows, direction (0,0) is invalid
#
#   returns a data.frame with R rows and these columns:
#       direction   the given matrix of directions
#       value       the value of the support function of the zonoid, in the given direction
#       argmax      the point on the boundary of the zonoid where the max is taken
#       arcs        number of arcs for argmax

support <- function( direction )
    {
    ok  = is.numeric(direction)  &&  0<length(direction)

    if( ! ok )
        {
        log_level( ERROR, "argument direction is invalid." )
        return(NULL)
        }

    if( ! is.matrix(direction) )
        dim(direction)  = c(1,length(direction))

    if( (ncol(direction) %% 2) == 0 )
        #   the number of columns is even, so append a column of 0s
        direction   = cbind( direction, 0 )

    m       = ncol(direction)   # m is odd

    count   = nrow(direction)

    value   = rep( NA_real_, count )
    argmax  = matrix( NA_real_, count, m )
    arcs    = rep( NA_integer_, count )

    #  convert normal to coefficients of a trig polynomial
    n       = (m-1L)/2L
    even    = 2L * ( 1:n )
    odd     = even - 1L

    for( i in 1:count )
        {
        normal  = direction[i, ]

        if( all(normal==0) )    next

        ablist  = list( a0=normal[m], a=normal[odd], b=normal[even] )   #; print( ablist )

        #   length(theta) is guaranteed to be even
        theta   = trigpolyroot( ablist )   #; print( theta )

        arcmat = arcsfromendpointsandpoly( theta, ablist )  #; print( arcmat )

        p   = boundaryfromarcs( arcmat, n=n )       #;  print(p)

        #z   = res$point

        value[i]    = sum( normal * p )
        argmax[i, ] = p
        arcs[i]     = length(theta)/2L      #; nrow( arcmat )
        }

    rnames  = rownames(direction)
    if( is.null(rnames)  ||  anyDuplicated(rnames)!=0 ) rnames = 1:count

    out = data.frame( row.names=rnames )
    out$direction   = direction
    out$value       = value
    out$argmax      = argmax
    out$arcs        = arcs

    return(out)
    }

Try the polarzonoid package in your browser

Any scripts or data that you put into this service are public.

polarzonoid documentation built on June 13, 2025, 9:08 a.m.