mono_cubic2.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. """
  2. Function supporting monotonic cubic polynomials:
  3. second parametrization
  4. We define the polynomial:
  5. poly(x) = sum_i beta_i * x^i
  6. where the coefficients beta_i are parameterized by the vector 'pars'.
  7. """
  8. import numpy as np
  9. # Forward map: Parameters to polynomial coefficients
  10. def forward_map(pars):
  11. """
  12. Converts parameters (pars = [C, epsilon, k1, k2]) into polynomial coefficients (beta = [d, c, b, a]).
  13. Polynomial Definition:
  14. - d = C: Constant term of the polynomial.
  15. - c = k2^2 + epsilon^2: Coefficient of the linear term.
  16. - b = k1 * k2: Coefficient of the quadratic term.
  17. - a = k1**2 / 3: Coefficient of the cubic term.
  18. Parameters:
  19. - pars: NumPy array of parameters [C, epsilon, k1, k2].
  20. Returns:
  21. - beta: NumPy array of polynomial coefficients [d, c, b, a].
  22. """
  23. C, epsilon, k1, k2 = pars # Unpack the parameter vector
  24. # Compute coefficients
  25. d = C
  26. c = k2**2 + epsilon**2
  27. b = k1 * k2
  28. a = k1**2 / 3
  29. # Return coefficients as a NumPy array
  30. beta = np.array([d, c, b, a])
  31. return beta
  32. # Jacobian of the forward map: First-order derivatives
  33. def forward_map_jacobian(pars):
  34. """
  35. Computes the Jacobian matrix of the forward map analytically.
  36. Parameters:
  37. - pars: NumPy array of parameters [C, epsilon, k1, k2].
  38. Returns:
  39. - J: NumPy 4x4 Jacobian matrix, where J[i, j] = d(beta[i])/d(pars[j]).
  40. """
  41. C, epsilon, k1, k2 = pars # Unpack parameters
  42. # Initialize Jacobian matrix
  43. J = np.zeros((4, 4)) # 4x4 matrix
  44. # Partial derivatives for d = C
  45. J[0, 0] = 1 # d(d)/dC
  46. J[0, 1] = 0 # d(d)/d(epsilon)
  47. J[0, 2] = 0 # d(d)/d(k1)
  48. J[0, 3] = 0 # d(d)/d(k2)
  49. # Partial derivatives for c = k2^2 + epsilon^2
  50. J[1, 0] = 0 # d(c)/dC
  51. J[1, 1] = 2 * epsilon # d(c)/d(epsilon)
  52. J[1, 2] = 0 # d(c)/d(k1)
  53. J[1, 3] = 2 * k2 # d(c)/d(k2)
  54. # Partial derivatives for b = k1 * k2
  55. J[2, 0] = 0 # d(b)/dC
  56. J[2, 1] = 0 # d(b)/d(epsilon)
  57. J[2, 2] = k2 # d(b)/d(k1)
  58. J[2, 3] = k1 # d(b)/d(k2)
  59. # Partial derivatives for a = k1^2 / 3
  60. J[3, 0] = 0 # d(a)/dC
  61. J[3, 1] = 0 # d(a)/d(epsilon)
  62. J[3, 2] = 2 * k1 / 3 # d(a)/d(k1)
  63. J[3, 3] = 0 # d(a)/d(k2)
  64. return J
  65. # Hessian of the forward map: Second-order derivatives
  66. def forward_map_hessian(pars):
  67. """
  68. Computes the Hessian tensor of the forward map analytically.
  69. Parameters:
  70. - pars: NumPy array of parameters [C, epsilon, k1, k2].
  71. Returns:
  72. - H: NumPy 4x4x4 Hessian tensor, where H[i, j, k] = d^2(beta[i])/d(pars[j])d(pars[k]).
  73. """
  74. C, epsilon, k1, k2 = pars # Unpack parameters
  75. # Initialize Hessian tensor (4 x 4 x 4)
  76. H = np.zeros((4, 4, 4))
  77. # Hessian for d = C: All second derivatives are zero
  78. # Already H[0, :, :] is initialized to zero
  79. # Hessian for c = k2^2 + epsilon^2
  80. H[1, 1, 1] = 2 # d^2(c)/d(epsilon^2)
  81. H[1, 3, 3] = 2 # d^2(c)/d(k2^2)
  82. # Hessian for b = k1 * k2: All second derivatives are zero
  83. # Already H[2, :, :] is initialized to zero
  84. # Hessian for a = k1^2 / 3
  85. H[3, 2, 2] = 2 / 3 # d^2(a)/d(k1^2)
  86. return H
  87. # Backward map: Polynomial coefficients to parameters
  88. def backward_map(beta, only_one = True):
  89. """
  90. Computes the parameters (pars = [C, epsilon, k1, k2]) from the polynomial coefficients (beta = [d, c, b, a]).
  91. Polynomial Definition:
  92. - d = C: Constant term of the polynomial.
  93. - c = k2^2 + epsilon^2: Used to recover k2 and epsilon.
  94. - b = k1 * k2: Used to recover k1 and k2.
  95. - a = k1^2 / 3: Used to recover k1.
  96. Parameters:
  97. - beta: NumPy array of polynomial coefficients [d, c, b, a].
  98. Returns:
  99. - List of possible parameter sets [(C, epsilon, k1, k2)].
  100. """
  101. d, c, b, a = beta # Unpack the coefficients
  102. # Recover k1 from a (two possible values due to ± sqrt)
  103. if a < 0:
  104. raise ValueError("Coefficient 'a' must be non-negative for monotonic polynomials.")
  105. k1_options = np.unique([np.sqrt(3 * a), -np.sqrt(3 * a)]) # Two possible k1 values
  106. possible_parameters = []
  107. # For each possible k1, compute k2 and epsilon
  108. for k1 in k1_options:
  109. k2 = None
  110. if k1 != 0: # Ensure k1 is non-zero (avoids division by zero)
  111. k2 = b / k1 # Compute k2 from b and k1
  112. elif b == 0:
  113. k2 = 0
  114. if k2 is None: continue
  115. # Check if c >= k2^2 for valid epsilon computation
  116. if c >= k2**2:
  117. epsilon_options = np.unique([np.sqrt(c - k2**2), -np.sqrt(c - k2**2)]) # Two possible epsilon values
  118. for epsilon in epsilon_options:
  119. # Constant term d maps directly to C
  120. C = d
  121. sol = np.array([C, epsilon, k1, k2])
  122. if only_one: return sol
  123. possible_parameters.append(sol)
  124. return possible_parameters
  125. # -------------------------
  126. # Round-trip test
  127. # -------------------------
  128. if __name__ == "__main__":
  129. pars_original = np.array([1.0, 2.0, 3.0, 4.0])
  130. beta = forward_map(pars_original)
  131. pars_recovered = backward_map(beta)
  132. print("Original pars: ", pars_original)
  133. print("Beta: ", beta)
  134. print("Recovered pars:", pars_recovered)
  135. print("Difference: ", pars_recovered - pars_original)
  136. print("\nJacobian at pars:")
  137. print(forward_map_jacobian(pars_original))
  138. print("\nHessian for beta1:")
  139. print(forward_map_hessian(pars_original)[1])
  140. print("\nLinear func:")
  141. lin_fun_beta = [1,0.2,0,0]
  142. only_one = False
  143. lin_fun_pars = backward_map(lin_fun_beta, only_one=only_one)
  144. lin_fun_beta_recover = lin_fun_pars if only_one else np.unique([forward_map(pars) for pars in lin_fun_pars], axis=0)
  145. print(f" {only_one = }")
  146. print(" lin_fun_beta:", lin_fun_beta)
  147. print(" backwards:", lin_fun_pars)
  148. print(" forwards:", lin_fun_beta_recover)