mono_cubic2.py 5.9 KB

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