mono_cubic2.py 6.0 KB

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