complex_univar_taylor.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. # complex_univar_taylor.py: class implementing Taylor expansions of a unique complex variable
  2. # Copyright 2022 Romain Serra
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from typing import List, Iterable, Optional
  15. import numpy as np
  16. from numba import njit
  17. from swiftt.taylor.complex_multivar_taylor import ComplexMultivarTaylor, Scalar
  18. from swiftt.taylor.taylor_expans_abstract import TaylorExpansAbstract, default_unknown_name, default_inverse_name
  19. from swiftt.taylor.tables import factorials
  20. class ComplexUnivarTaylor(ComplexMultivarTaylor):
  21. """Class for Taylor expansions of a single complex variable.
  22. """
  23. def __init__(self, order: int, var_names: List[str] = default_unknown_name) -> None:
  24. """Constructor for Taylor expansions of a single complex variable. Result has no assigned coefficients.
  25. Args:
  26. order (int): order of expansion.
  27. var_names (List[str]): name of variable.
  28. """
  29. ComplexMultivarTaylor.__init__(self, 1, order, var_names)
  30. def create_expansion_with_coeff(self, coeff: Iterable) -> "ComplexUnivarTaylor":
  31. """Method to create a new Taylor expansion (in same algebra than self) from given coefficients. Overwrites
  32. multivariate implementation to make sure the correct object is created (dedicated to univariate expansions).
  33. Args:
  34. coeff (Iterable[complex]): coefficients of new expansion.
  35. Returns:
  36. ComplexUnivarTaylor: Taylor expansion corresponding to inputted coefficients.
  37. """
  38. expansion = self.__class__(self._order, self._var_names)
  39. expansion.coeff = coeff
  40. return expansion
  41. def get_mth_term_coeff(self, order: int) -> Scalar:
  42. """Method to get the coefficient corresponding to a given order.
  43. Args:
  44. order (int): order of requested coefficient.
  45. Returns:
  46. Union[complex, float]: coefficient for input order.
  47. """
  48. if 0 <= order <= self._order:
  49. return self._coeff[order]
  50. raise IndexError("Input order must be a positive integer at most equal to the order of the algebra")
  51. def __mul__(self, other) -> "ComplexUnivarTaylor":
  52. """Method defining right-hand side multiplication. It works for the external multiplication i.e. with scalars
  53. and for the internal one, that is between expansions. Overwrites the multivariate implementation for
  54. performance.
  55. Args:
  56. other (Union[ComplexUnivarTaylor, complex, float]): quantity to be multiplied with.
  57. Returns:
  58. ComplexUnivarTaylor: multiplied objects.
  59. """
  60. if isinstance(other, ComplexUnivarTaylor):
  61. if self.is_in_same_algebra(other):
  62. multiplied_coeff = self._mul_expansion(other)
  63. return self.create_expansion_with_coeff(multiplied_coeff)
  64. raise ValueError("Expansions to be multiplied are not from the same algebra")
  65. # scalar case
  66. return self.create_expansion_with_coeff(other * self._coeff)
  67. @staticmethod
  68. @njit(cache=True)
  69. def mul_univar(coeff: np.ndarray, other_coeff: np.ndarray) -> np.ndarray:
  70. """Static method transforming two series of coefficients into the coefficients of the product of univariate
  71. Taylor expansions. It emulates the polynomial product and the truncation at the same time. Method is static so
  72. that it can be replaced by faster C code if applicable.
  73. Args:
  74. coeff (numpy.ndarray): first set of coefficients.
  75. other_coeff (numpy.ndarray): second set of coefficients.
  76. Returns:
  77. numpy.ndarray: coefficient corresponding to product.
  78. """
  79. multiplied_coeff = coeff[0] * other_coeff
  80. multiplied_coeff[1:] += coeff[1:] * other_coeff[0]
  81. other_nonconst_coeff_reverted = np.flip(other_coeff[1:])
  82. for i in range(2, coeff.shape[0]):
  83. multiplied_coeff[i] += coeff[1:i].dot(other_nonconst_coeff_reverted[-i + 1:])
  84. return multiplied_coeff
  85. def _mul_expansion(self, other: "ComplexUnivarTaylor") -> np.ndarray:
  86. """Wrapper to static method on coefficients to multiply univariate expansion.
  87. Args:
  88. other (ComplexUnivarTaylor): Taylor expansion to be multiplied with.
  89. Returns:
  90. numpy.ndarray: coefficients of product of Taylor expansions.
  91. """
  92. return self.mul_univar(self._coeff, other._coeff)
  93. @staticmethod
  94. @njit(cache=True)
  95. def pow2_univar(coeff: np.ndarray, half_order: int) -> np.ndarray:
  96. """Static method transforming coefficients into the coefficients of the square of a univariate
  97. Taylor expansion. It emulates the polynomial product and the truncation at the same time. Method is static so
  98. that it can be replaced by faster C code if applicable.
  99. Args:
  100. coeff (numpy.ndarray): first set of coefficients.
  101. half_order (int): half order.
  102. Returns:
  103. numpy.ndarray: coefficients corresponding to square.
  104. """
  105. twice_coeff = 2. * coeff
  106. new_coeff = coeff[0] * twice_coeff
  107. squared_coeff = coeff[:half_order]**2
  108. new_coeff[0] = squared_coeff[0]
  109. for i in range(1, half_order):
  110. index = 2 * i
  111. new_coeff[index] += squared_coeff[i]
  112. new_coeff[i + 1:index] += twice_coeff[i] * coeff[1:i]
  113. dim = coeff.shape[0]
  114. for i in range(half_order, dim - 1):
  115. new_coeff[i + 1:] += twice_coeff[i] * coeff[1:dim-i]
  116. return new_coeff
  117. def pow2(self) -> "ComplexUnivarTaylor":
  118. """Method to raise Taylor expansion to the power 2 i.e. compute its multiplicative square.
  119. Returns:
  120. ComplexUnivarTaylor: Taylor expansion raised to power 2.
  121. """
  122. coeff = self.pow2_univar(self._coeff, self.half_order + 1)
  123. return self.create_expansion_with_coeff(coeff)
  124. def __call__(self, *args, **kwargs):
  125. """Method for calling the Taylor expansion. Wraps several possibilities: evaluation and composition with another
  126. expansion.
  127. Returns:
  128. Union[ComplexMultivarTaylor, complex, float]: Taylor expansion called on input.
  129. """
  130. other = args[0]
  131. return self.compose(other) if isinstance(other, ComplexMultivarTaylor) else self.pointwise_eval(other)
  132. def compose(self, other: ComplexMultivarTaylor) -> ComplexMultivarTaylor:
  133. """Method performing composition with inputted univariate Taylor expansion (must have the same order).
  134. Args:
  135. other (ComplexMultivarTaylor): Taylor expansion to be composed on the right-hand side.
  136. Returns:
  137. ComplexMultivarTaylor: composed expansion.
  138. """
  139. if other.is_const():
  140. return other.create_const_expansion(self.pointwise_eval(other.const))
  141. if other.order != self._order:
  142. raise ValueError("Expansions to be composed must have same order")
  143. if not other.is_nilpotent():
  144. raise ValueError("Right-hand-side expansion for composition must be nilpotent")
  145. # Horner's scheme
  146. composed = self._coeff[-1] * other
  147. composed.const = self._coeff[-2]
  148. for el in self._coeff[-3::-1]:
  149. composed *= other
  150. composed.const = el
  151. return composed
  152. def pointwise_eval(self, x: Scalar) -> Scalar:
  153. """Method for the evaluation of the Taylor expansion on a given point.
  154. Args:
  155. x (Union[complex, float]): point of evaluation.
  156. Returns:
  157. Union[complex, float]: Taylor expansion evaluated at given point.
  158. """
  159. if self.is_trivial():
  160. return self.const
  161. # Horner's scheme
  162. output = self._coeff[-1] * x + self._coeff[-2]
  163. for el in self._coeff[-3::-1]:
  164. output = output * x + el
  165. return output
  166. def massive_eval(self, x: np.ndarray) -> np.ndarray:
  167. """Method for the evaluation of the Taylor expansion on a range of points (vectorized evaluation).
  168. Args:
  169. x (numpy.ndarray): points of evaluation.
  170. Returns:
  171. numpy.ndarray: Taylor expansion evaluated at given points.
  172. """
  173. # in the uni-variate case, the point-wise evaluation method is already vectorized
  174. return np.array(self.pointwise_eval(x))
  175. def truncated(self, new_order: int) -> "ComplexUnivarTaylor":
  176. """Method for the truncation at a given order. Output lives in another algebra, of lower dimension.
  177. Overwrites multivariate implementation for performance.
  178. Args:
  179. new_order (int): order of algebra in which to truncate the Taylor expansion.
  180. Returns:
  181. ComplexUnivarTaylor: Taylor expansion truncated at input order.
  182. """
  183. if 0 <= new_order < self._order:
  184. truncated = self.__class__(new_order, self._var_names)
  185. truncated.coeff = self._coeff[:truncated.dim_alg]
  186. return truncated
  187. raise ValueError("Input order must be an integer between zero and order of initial algebra")
  188. def prolong(self, new_order: int) -> "ComplexUnivarTaylor":
  189. """Method for the prolongation (opposite of truncation) at a given order. Output lives in another algebra, of
  190. higher dimension. It is not a rigorous operation as the possible contributions within the former remainder are
  191. ignored. Overwrites multivariate implementation for performance.
  192. Args:
  193. new_order (int): order of algebra in which to prolong the Taylor expansion.
  194. Returns:
  195. ComplexUnivarTaylor: Taylor expansion prolonged at input order.
  196. """
  197. if new_order > self._order:
  198. prolonged = self.__class__(new_order, self._var_names)
  199. coeff = np.zeros(prolonged._dim_alg, dtype=self.var_type)
  200. coeff[:self._dim_alg] = np.array(self._coeff, dtype=self.var_type)
  201. prolonged.coeff = coeff
  202. return prolonged
  203. raise ValueError("New order must be larger than old one")
  204. def prolong_one_order(self) -> "ComplexUnivarTaylor":
  205. """Method for the prolongation (opposite of truncation) of one order. Output lives in another algebra, of
  206. higher dimension. It is not a rigorous operation as the possible contributions within the former remainder are
  207. ignored. Overwrites multivariate implementation for performance.
  208. Returns:
  209. ComplexUnivarTaylor: Taylor expansion prolonged by one order.
  210. """
  211. prolonged = self.__class__(self._order + 1, self._var_names)
  212. coeff = np.zeros(prolonged._dim_alg, dtype=self.var_type)
  213. coeff[:self._dim_alg] = np.array(self._coeff, dtype=self.var_type)
  214. prolonged.coeff = coeff
  215. return prolonged
  216. def rigorous_integ_once_wrt_var(self, index_var: int = 0) -> "ComplexUnivarTaylor":
  217. """Method performing integration with respect to a given unknown variable. The integration constant is zero.
  218. This transformation is rigorous as the order of the expansion is increased by one. In other words, the output
  219. lives in another algebra, of higher dimension. Overwrites multivariate implementation for performance.
  220. Args:
  221. index_var (int): variable number w.r.t. which integration needs to be performed.
  222. Returns:
  223. ComplexUnivarTaylor: integrated Taylor expansion w.r.t. input variable number.
  224. """
  225. integrated = self.__class__(self._order + 1, self._var_names)
  226. coeff = np.zeros(integrated._dim_alg, dtype=self._var_type)
  227. coeff[1:] = self._coeff / np.arange(1, integrated.dim_alg)
  228. integrated.coeff = coeff
  229. return integrated
  230. def integ_once_wrt_var(self, index_var: int = 0) -> "ComplexUnivarTaylor":
  231. """Method performing integration while remaining in the same algebra. This transformation is not rigorous in the
  232. sense that the order of the expansion should be increased by one. Overwrites multivariate implementation for
  233. performance.
  234. Args:
  235. index_var (int): variable number w.r.t. which integration needs to be performed. It is not used
  236. as the expansion is univariate and there is no choice.
  237. Returns:
  238. ComplexUnivarTaylor: integrated Taylor expansion.
  239. """
  240. new_coeff = np.zeros(self._dim_alg, dtype=self._var_type)
  241. new_coeff[1:] = self._coeff[:-1] / np.arange(1, self.dim_alg)
  242. return self.create_expansion_with_coeff(new_coeff)
  243. def deriv_once_wrt_var(self, index_var: int = 0) -> "ComplexUnivarTaylor":
  244. """Method performing differentiation while remaining in the same algebra. This transformation is not rigorous in
  245. the sense that the order of the expansion should be decreased by one. Overwrites multivariate implementation
  246. for performance.
  247. Args:
  248. index_var (int): variable number w.r.t. which differentiation needs to be performed. It is not used
  249. as the expansion is univariate and there is no choice.
  250. Returns:
  251. ComplexUnivarTaylor: differentiated Taylor expansion.
  252. """
  253. new_coeff = np.zeros(self._dim_alg, dtype=self._var_type)
  254. new_coeff[:-1] = self._coeff[1:] * np.arange(1, self.dim_alg)
  255. return self.create_expansion_with_coeff(new_coeff)
  256. def get_all_partial_deriv_up_to(self, order: int) -> np.ndarray:
  257. """Method returning the derivatives of the expansion up to a given order. In other words, it converts
  258. the polynomial coefficients by multiplying them with factorials. Overwrites the multivariate implementation
  259. for performance.
  260. Args:
  261. order (int): order (included) up to which derivatives need to be outputted.
  262. Returns:
  263. numpy.ndarray: derivatives up to input order.
  264. """
  265. if 0 <= order <= self._order:
  266. output = np.array(self._coeff[:order + 1], dtype=self.var_type)
  267. return output * factorials(order)
  268. raise ValueError("Order of differentiation must be non-negative and not exceed order of algebra")
  269. def get_mth_deriv(self, m: int) -> Scalar:
  270. """Method returning a specific derivative of the expansion i.e. at a specified order.
  271. Args:
  272. m (int): order of requested derivative.
  273. Returns:
  274. Union[complex, float]: derivative at specified order.
  275. """
  276. return self.get_all_partial_deriv_up_to(m)[-1]
  277. def divided_by_var(self, index_var: int = 0) -> "ComplexUnivarTaylor":
  278. """Method returning the Taylor expansion divided by the variable. This is not a rigorous operation as the order
  279. should be decreased by one.
  280. Args:
  281. index_var (str): index of variable, not used in the univariate case.
  282. Returns:
  283. ComplexUnivarTaylor: Taylor expansion divided by variable.
  284. """
  285. if self.const != 0.:
  286. raise ValueError("Cannot divide by variable if constant term is not zero.")
  287. coeff = np.zeros(self._dim_alg, dtype=self._var_type)
  288. coeff[:self.order] = np.array(self._coeff[1:])
  289. return self.create_expansion_with_coeff(coeff)
  290. def compo_inverse(self, name_inverse: Optional[str] = None) -> "ComplexUnivarTaylor":
  291. """Method returning the inverse of the univariate Taylor expansion from the point of view of composition, i.e.
  292. so that composed with self it gives the monomial X. This is also known as series reversion.
  293. The computation is done iteratively, starting from the inversion of the first-order truncated expansion.
  294. See the work of M. Berz.
  295. Args:
  296. name_inverse (str): name of inverse variable.
  297. Returns:
  298. ComplexUnivarTaylor: composition-inverse of Taylor expansion.
  299. """
  300. if not self.is_nilpotent():
  301. raise ValueError("Expansion to be inverted cannot have non-zero constant term")
  302. if name_inverse is None:
  303. name_inverse = default_inverse_name
  304. coeff = np.zeros(self._dim_alg, dtype=self._var_type)
  305. coeff[1] = 1. / self._coeff[1]
  306. invlin = self.create_expansion_with_coeff(coeff)
  307. if self._order == 1:
  308. invlin.var_names = [name_inverse]
  309. return invlin
  310. # order is at least two
  311. inter = -self.get_nonlin_part()
  312. coeff[1] = 1.
  313. identity = self.create_expansion_with_coeff(coeff)
  314. inverted_expansion = invlin.compose(identity + inter.compose(invlin))
  315. for __ in range(3, self._dim_alg):
  316. inverted_expansion = invlin.compose(identity + inter.compose(inverted_expansion))
  317. inverted_expansion.var_names = [name_inverse]
  318. return inverted_expansion
  319. @property
  320. def effective_order(self) -> int:
  321. """Method returning the effective order of the expansion, that is the highest order with non-zero coefficients.
  322. Overwrites the multivariate implementation for simplicity.
  323. Returns:
  324. int: effective order of Taylor expansion.
  325. """
  326. for i, el in zip(range(self._dim_alg - 1, -1, -1), self._coeff[::-1]):
  327. if el != 0.:
  328. return i
  329. return 0
  330. @property
  331. def total_depth(self) -> int:
  332. """Method returning the total depth of the Taylor expansion, that is the lowest order with non-zero coefficient.
  333. Overwrites the multivariate implementation for simplicity.
  334. Returns:
  335. int: total depth of Taylor expansion.
  336. """
  337. for i, el in enumerate(self._coeff):
  338. if el != 0.:
  339. return i
  340. return self._dim_alg
  341. @property
  342. def remainder_term(self) -> str:
  343. return TaylorExpansAbstract.landau_univar(self._order, self._var_names)
  344. def __str__(self) -> str:
  345. """Method to cast Taylor expansion as a string. Overwrites parent implementation that assumes more than one
  346. variable.
  347. Returns:
  348. str: string representing the Taylor expansion.
  349. """
  350. string = str(self.const)
  351. if not self.is_trivial():
  352. var_name = self._var_names[0]
  353. if self._coeff[1] != 0.:
  354. string += " + " + str(self._coeff[1]) + " * " + var_name
  355. for j, el in enumerate(self._coeff[2:], 2):
  356. if el != 0.:
  357. string += " + " + str(el) + " * " + var_name + "**" + str(j)
  358. return string + " + " + self.remainder_term
  359. def var_inserted(self, index_new_var: int, unknown_name: Optional[str] = None) -> ComplexMultivarTaylor:
  360. """Method for the addition of a new variable. Output lives in another algebra, of higher dimension. All its
  361. terms associated with the new variable are zero and the other ones are identical to original expansion.
  362. Overwrites the parent implementation for performance.
  363. Args:
  364. index_new_var (int): index of new variable to be added.
  365. unknown_name (str): name of new variable.
  366. Returns:
  367. ComplexMultivarTaylor: Taylor expansion with an additional variable.
  368. """
  369. if unknown_name is None:
  370. unknown_name = default_unknown_name + str(2)
  371. if unknown_name in self.var_names:
  372. raise ValueError("Name of new variable already exists.")
  373. if index_new_var == 1:
  374. new_index_old_var = 0
  375. new_var_names = [self._var_names[0], unknown_name]
  376. else:
  377. new_index_old_var = 1
  378. new_var_names = [unknown_name, self._var_names[0]]
  379. new_expansion = ComplexMultivarTaylor(2, self._order, new_var_names)
  380. new_coeff = np.zeros(new_expansion.dim_alg, dtype=self._var_type)
  381. for exponent, index_coeff in new_expansion.get_mapping_monom().items():
  382. if exponent[index_new_var] == 0:
  383. new_coeff[index_coeff] = self._coeff[exponent[new_index_old_var]]
  384. new_expansion.coeff = new_coeff
  385. return new_expansion
  386. def var_eval(self, index_var: int, value: Scalar) -> "ComplexMultivarTaylor":
  387. if index_var != 0:
  388. raise IndexError("The inputted index does not correspond to any variable of the expansion.")
  389. return self.create_const_expansion(self.pointwise_eval(value))