flapjax 1.2.0.dev0__tar.gz → 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/PKG-INFO +3 -3
  2. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/packaging/full/README.md +0 -8
  3. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/packaging/full/pyproject.toml +3 -3
  4. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/pyproject.toml +3 -3
  5. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/aero/linear/linear_uvlm.py +469 -27
  6. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/aero/utils.py +43 -7
  7. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/aero/uvlm.py +60 -32
  8. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/coupled/__init__.py +1 -0
  9. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/coupled/coupled.py +5 -2
  10. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/coupled/data_structures.py +5 -3
  11. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/coupled/gradients/coupled.py +106 -69
  12. flapjax-1.2.2/src/flapjax/coupled/gradients/utils.py +78 -0
  13. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/coupled/linear/linear_coupled.py +84 -4
  14. flapjax-1.2.2/src/flapjax/coupled/linear_aero_coupled.py +364 -0
  15. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/simple_hale/simple_hale.py +14 -7
  16. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/structure/beam.py +116 -26
  17. flapjax-1.2.2/src/flapjax/structure/constraints.py +191 -0
  18. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/structure/gradients/beam.py +1 -0
  19. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/aero/test_infinite_wing_polar.py +39 -0
  20. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/conftest.py +6 -0
  21. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/coupled/dynamic/test_dynamic_gust_adjoint.py +2 -4
  22. flapjax-1.2.2/src/flapjax/tests/coupled/dynamic/test_linear_aero_equilibrium.py +85 -0
  23. flapjax-1.2.2/src/flapjax/tests/structure/dynamic_primal/test_prescribed_motion.py +135 -0
  24. flapjax-1.2.2/src/flapjax/tests/structure/static_primal/test_free_free_modes.py +95 -0
  25. flapjax-1.2.2/src/flapjax/tests/structure/static_primal/test_nodal_constraints.py +166 -0
  26. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/utils/linear.py +6 -6
  27. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/.gitignore +0 -0
  28. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/LICENSE +0 -0
  29. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/README.md +0 -0
  30. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/figures/beam_transform.png +0 -0
  31. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/figures/primal_solve.png +0 -0
  32. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/figures/uvlm_grid.png +0 -0
  33. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/index.md +0 -0
  34. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/javascripts/mathjax.js +0 -0
  35. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/reference/aero.md +0 -0
  36. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/reference/algebra.md +0 -0
  37. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/reference/coupled.md +0 -0
  38. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/reference/index.md +0 -0
  39. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/reference/structure.md +0 -0
  40. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/reference/utils.md +0 -0
  41. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/stylesheets/extra.css +0 -0
  42. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/theory/adjoint.md +0 -0
  43. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/theory/beam.md +0 -0
  44. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/theory/coupling.md +0 -0
  45. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/theory/index.md +0 -0
  46. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/theory/linear.md +0 -0
  47. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/theory/uvlm.md +0 -0
  48. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/flying_spaghetti.ipynb +0 -0
  49. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/flying_spaghetti_optimise.ipynb +0 -0
  50. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/geradin_beam_adjoint.ipynb +0 -0
  51. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/geradin_beam_static.ipynb +0 -0
  52. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/index.md +0 -0
  53. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/patil_wing_control.ipynb +0 -0
  54. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/simple_hale_gust.ipynb +0 -0
  55. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/straight_pazy_deformed_modes.ipynb +0 -0
  56. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/straight_pazy_flutter.ipynb +0 -0
  57. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/straight_pazy_lco.ipynb +0 -0
  58. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/straight_pazy_static_deflection.ipynb +0 -0
  59. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/swept_pazy_deformed_modes.ipynb +0 -0
  60. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/swept_pazy_flutter.ipynb +0 -0
  61. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/swept_pazy_lco.ipynb +0 -0
  62. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/docs/tutorials/swept_pazy_static_deflection.ipynb +0 -0
  63. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/mkdocs.yml +0 -0
  64. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/packaging/full/LICENSE +0 -0
  65. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/__init__.py +0 -0
  66. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/aero/__init__.py +0 -0
  67. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/aero/aic.py +0 -0
  68. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/aero/data_structures.py +0 -0
  69. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/aero/flowfields.py +0 -0
  70. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/aero/gradients/__init__.py +0 -0
  71. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/aero/gradients/data_structures.py +0 -0
  72. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/aero/linear/__init__.py +0 -0
  73. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/aero/linear/data_structures.py +0 -0
  74. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/algebra/__init__.py +0 -0
  75. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/algebra/array_utils.py +0 -0
  76. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/algebra/base.py +0 -0
  77. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/algebra/integration.py +0 -0
  78. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/algebra/se3.py +0 -0
  79. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/algebra/so3.py +0 -0
  80. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/algebra/test_routines.py +0 -0
  81. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/coupled/gradients/__init__.py +0 -0
  82. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/coupled/gradients/data_structures.py +0 -0
  83. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/coupled/linear/__init__.py +0 -0
  84. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/coupled/linear/data_structures.py +0 -0
  85. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/__init__.py +0 -0
  86. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/cantilever_wing.py +0 -0
  87. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/flying_spaghetti/__init__.py +0 -0
  88. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/flying_spaghetti/flying_spaghetti.py +0 -0
  89. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/flying_spaghetti/free_dynamic.ipynb +0 -0
  90. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/flying_spaghetti/mass_optimisation.ipynb +0 -0
  91. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/geradin_beam/__init__.py +0 -0
  92. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/geradin_beam/adjoint_gradients.ipynb +0 -0
  93. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/geradin_beam/geradin_beam.py +0 -0
  94. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/geradin_beam/static_deformation.ipynb +0 -0
  95. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/ifasd_aviation_2026/__init__.py +0 -0
  96. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/ifasd_aviation_2026/cantilever_wing_adjoint.py +0 -0
  97. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/ifasd_aviation_2026/optimal_roll.py +0 -0
  98. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/panel_scaling_profile.py +0 -0
  99. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/patil_wing/__init__.py +0 -0
  100. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/patil_wing/open_loop_control.ipynb +0 -0
  101. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/patil_wing/patil_wing.py +0 -0
  102. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/__init__.py +0 -0
  103. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/base.py +0 -0
  104. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/straight/__init__.py +0 -0
  105. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/straight/data/__init__.py +0 -0
  106. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/straight/data/prepazy_properties.py +0 -0
  107. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/straight/data/technion_pazy_properties.py +0 -0
  108. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/straight/deformed_modes.ipynb +0 -0
  109. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/straight/flutter.ipynb +0 -0
  110. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/straight/lco.ipynb +0 -0
  111. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/straight/pazy_wing.py +0 -0
  112. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/straight/static_deflection.ipynb +0 -0
  113. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/swept/__init__.py +0 -0
  114. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/swept/data/__init__.py +0 -0
  115. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/swept/data/properties.py +0 -0
  116. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/swept/deformed_modes.ipynb +0 -0
  117. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/swept/flutter.ipynb +0 -0
  118. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/swept/lco.ipynb +0 -0
  119. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/swept/static_deflection.ipynb +0 -0
  120. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/pazy/swept/swept_pazy_wing.py +0 -0
  121. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/scitech_abstract_2027/__init__.py +0 -0
  122. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/scitech_abstract_2027/linear_uvlm_case.py +0 -0
  123. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/simple_hale/__init__.py +0 -0
  124. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/models/simple_hale/parallel_gust.ipynb +0 -0
  125. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/plotting/__init__.py +0 -0
  126. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/plotting/aerogrid.py +0 -0
  127. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/plotting/beam.py +0 -0
  128. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/plotting/modal.py +0 -0
  129. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/plotting/pvd.py +0 -0
  130. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/structure/__init__.py +0 -0
  131. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/structure/data_structures.py +0 -0
  132. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/structure/gradients/__init__.py +0 -0
  133. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/structure/gradients/data_structures.py +0 -0
  134. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/structure/linear/__init__.py +0 -0
  135. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/structure/linear/data_structures.py +0 -0
  136. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/structure/linear/linear_beam.py +0 -0
  137. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/structure/time_integration.py +0 -0
  138. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/structure/utils.py +0 -0
  139. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/aero/test_linear_planar_wing.py +0 -0
  140. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/aero/test_mirrored_wing.py +0 -0
  141. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/aero/test_static_no_wake.py +0 -0
  142. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/aero/test_variable_wake_disc.py +0 -0
  143. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/algebra/test_beam_algebra.py +0 -0
  144. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/algebra/test_kernels.py +0 -0
  145. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/coupled/dynamic/test_dynamic_equilibrium.py +0 -0
  146. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/coupled/dynamic/test_dynamic_equilibrium_adjoint.py +0 -0
  147. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/coupled/dynamic/test_parallel_cantilever.py +0 -0
  148. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/coupled/static/test_pazy.py +0 -0
  149. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/coupled/static/test_static_adjoint.py +0 -0
  150. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/plotting/test_beam_plot.py +0 -0
  151. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/dynamic_linear/test_oscillating_cantilever.py +0 -0
  152. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/dynamic_primal/test_applied_force_point.py +0 -0
  153. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/dynamic_primal/test_const_initial_linear_velocity.py +0 -0
  154. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/dynamic_primal/test_const_initial_rotational_velocity.py +0 -0
  155. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/dynamic_primal/test_const_initial_velocity_lumped_mass.py +0 -0
  156. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/dynamic_primal/test_gravity_beam_drop.py +0 -0
  157. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/dynamic_primal/test_gravity_point_drop.py +0 -0
  158. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/dynamic_primal/test_parallel_spaghetti.py +0 -0
  159. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/dynamic_primal/test_two_lumped_mass_const_rot_velocity.py +0 -0
  160. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/dynamic_tangent/test_beam_dynamic_adjoint.py +0 -0
  161. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/dynamic_tangent/test_lumped_mass_dynamic_adjoint.py +0 -0
  162. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/static_primal/test_geradin_beam.py +0 -0
  163. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/static_primal/test_mass_gravity.py +0 -0
  164. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/static_primal/test_matrices.py +0 -0
  165. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/static_primal/test_modal.py +0 -0
  166. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/static_primal/test_multi_element_beam.py +0 -0
  167. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/static_primal/test_pazy_modes.py +0 -0
  168. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/static_primal/test_two_node_beam.py +0 -0
  169. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/tests/structure/static_tangent/test_geradin_gradients.py +0 -0
  170. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/utils/__init__.py +0 -0
  171. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/utils/constants.py +0 -0
  172. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/utils/data_structures.py +0 -0
  173. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/utils/print_utils.py +0 -0
  174. {flapjax-1.2.0.dev0 → flapjax-1.2.2}/src/flapjax/utils/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flapjax
3
- Version: 1.2.0.dev0
3
+ Version: 1.2.2
4
4
  Summary: Solver for computing aeroelastic responses and corresponding design derivatives.
5
5
  Project-URL: Homepage, https://ben-l-p.github.io/flapjax/
6
6
  Project-URL: Documentation, https://ben-l-p.github.io/flapjax/
@@ -19,8 +19,8 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Topic :: Scientific/Engineering
20
20
  Classifier: Topic :: Scientific/Engineering :: Physics
21
21
  Requires-Python: >=3.12
22
- Requires-Dist: jax>=0.4.32
23
- Requires-Dist: matplotlib>=3.8
22
+ Requires-Dist: jax>=0.8.1
23
+ Requires-Dist: matplotlib>=3.8.4
24
24
  Requires-Dist: numpy>=2.0
25
25
  Requires-Dist: scipy>=1.13
26
26
  Requires-Dist: vtk>=9.4
@@ -27,11 +27,3 @@ pip install flapjax-full # includes tests and tutorial notebooks
27
27
  ```bash
28
28
  pytest --pyargs flapjax
29
29
  ```
30
-
31
- ## Locating the notebooks
32
-
33
- The tutorial notebooks ship inside the installed package. Find them with:
34
-
35
- ```bash
36
- python -c "import flapjax, pathlib; print(pathlib.Path(flapjax.__file__).parent / 'models')"
37
- ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flapjax-full"
3
- version = "1.2.0.dev0"
3
+ version = "1.2.2"
4
4
  description = "FLAPJAX with tests and tutorial notebooks bundled. Install this OR flapjax, not both."
5
5
  authors = [{ name = "Ben Preston", email = "b.preston23@imperial.ac.uk" }]
6
6
  keywords = ["aeroelasticity", "unsteady vortex lattice method", "nonlinear beam", "adjoint methods"]
@@ -19,11 +19,11 @@ readme = "README.md"
19
19
  license = "MIT"
20
20
  license-files = ["LICENSE"]
21
21
  dependencies = [
22
- "jax>=0.4.32",
22
+ "jax>=0.8.1",
23
23
  "numpy>=2.0",
24
24
  "vtk>=9.4",
25
25
  "scipy>=1.13",
26
- "matplotlib>=3.8",
26
+ "matplotlib>=3.8.4",
27
27
  "pytest>=9.0",
28
28
  ]
29
29
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flapjax"
3
- version = "1.2.0.dev0"
3
+ version = "1.2.2"
4
4
  description = "Solver for computing aeroelastic responses and corresponding design derivatives."
5
5
  authors = [{ name = "Ben Preston", email = "b.preston23@imperial.ac.uk" }]
6
6
  keywords = ["aeroelasticity", "unsteady vortex lattice method", "nonlinear beam", "adjoint methods"]
@@ -19,11 +19,11 @@ readme = "README.md"
19
19
  license = "MIT"
20
20
  license-files = ["LICENSE"]
21
21
  dependencies = [
22
- "jax>=0.4.32",
22
+ "jax>=0.8.1",
23
23
  "numpy>=2.0",
24
24
  "vtk>=9.4",
25
25
  "scipy>=1.13",
26
- "matplotlib>=3.8",
26
+ "matplotlib>=3.8.4",
27
27
  ]
28
28
 
29
29
  [project.urls]
@@ -1,11 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- from collections.abc import Sequence
4
+ from collections.abc import Callable, Sequence
5
+ from dataclasses import dataclass
5
6
  from pathlib import Path
6
- from typing import TYPE_CHECKING, Literal
7
+ from typing import TYPE_CHECKING, Any, Literal
7
8
 
8
- import jax
9
9
  import jax.numpy as jnp
10
10
  from jax import Array, vmap
11
11
 
@@ -25,7 +25,12 @@ from flapjax.aero.utils import (
25
25
  calculate_steady_forcing,
26
26
  compute_nc,
27
27
  )
28
- from flapjax.algebra.array_utils import ArrayList, split_to_vertex
28
+ from flapjax.algebra.array_utils import (
29
+ ArrayList,
30
+ construct_named_block_jacobian,
31
+ split_to_vertex,
32
+ )
33
+ from flapjax.algebra.base import ADMode, jacrev_custom
29
34
  from flapjax.utils.linear import (
30
35
  LinearComponent,
31
36
  LinearModel,
@@ -40,6 +45,37 @@ if TYPE_CHECKING:
40
45
  type LinearWakeType = Literal["frozen", "prescribed", "free"]
41
46
 
42
47
 
48
+ @dataclass
49
+ class LinearInputProjection:
50
+ r"""
51
+ Projection of grid coordinates and velocities from a lower-dimensional input space (e.g. beam DOFs) to
52
+ ``(zeta_b_np1_vec, zeta_b_dot_np1_vec)``.
53
+ :param arg_names: Names of the projected input dofs (used as column labels).
54
+ :param arg_sizes: Sizes of each projected input dof block.
55
+ :param arg_refs: Reference values for the projected inputs.
56
+ :param to_zeta: Callable ``(**{arg_name: vec}) -> (zeta_b_vec, zeta_b_dot_vec)`` returning their total values.
57
+ """
58
+
59
+ arg_names: tuple[str, ...]
60
+ arg_sizes: tuple[int, ...]
61
+ arg_refs: tuple[Array, ...]
62
+ to_zeta: Callable[..., tuple[Array, Array]]
63
+
64
+
65
+ @dataclass
66
+ class LinearOutputProjection:
67
+ r"""
68
+ Projection of forces from the full aerodynamic grid to a single output space.
69
+ :param name: Name of the projected output block (used as row label).
70
+ :param size: Size of the projected output.
71
+ :param to_output: Callable ``(f_steady_vec, f_unsteady_vec | None) -> projected_vec``.
72
+ """
73
+
74
+ name: str
75
+ size: int
76
+ to_output: Callable[[Array, Array | None], Array]
77
+
78
+
43
79
  class LinearUVLM(
44
80
  LinearModel[
45
81
  AeroCase,
@@ -74,7 +110,6 @@ class LinearUVLM(
74
110
  :param wake_upwash: If true, include wake surface upwash velocities as inputs.
75
111
  :param unsteady_force: If true, include unsteady force.
76
112
  """
77
-
78
113
  # options
79
114
  self.prescribed_wake, self.free_wake = {
80
115
  "frozen": (False, False),
@@ -272,7 +307,7 @@ class LinearUVLM(
272
307
  """
273
308
 
274
309
  u_np1 = self._unpack_input_vector(u_vec)
275
- x_n = self._unpack_state_vector(x_vec)
310
+ x_n = self.unpack_state_vector(x_vec)
276
311
 
277
312
  assert isinstance(u_np1, AeroInputUnflattened) and isinstance(
278
313
  x_n, AeroStateUnflattened
@@ -282,7 +317,7 @@ class LinearUVLM(
282
317
 
283
318
  x_np1, y_n = self.step(u_np1=u_np1, x_n=x_n)
284
319
 
285
- return self._pack_state_vector(x_np1), self._pack_output_vector(y_n)
320
+ return self.pack_state_vector(x_np1), self.pack_output_vector(y_n)
286
321
 
287
322
  def step(
288
323
  self,
@@ -394,33 +429,440 @@ class LinearUVLM(
394
429
 
395
430
  return x_np1, y_n
396
431
 
397
- def linearise(self) -> LinearSystem:
432
+ def _step_from_vecs(
433
+ self,
434
+ gamma_b_n_vec: Array | None = None,
435
+ gamma_w_n_vec: Array | None = None,
436
+ gamma_b_nm1_vec: Array | None = None,
437
+ zeta_w_n_vec: Array | None = None,
438
+ zeta_b_n_vec: Array | None = None,
439
+ zeta_b_np1_vec: Array | None = None,
440
+ zeta_b_dot_np1_vec: Array | None = None,
441
+ nu_b_np1_vec: Array | None = None,
442
+ nu_w_np1_vec: Array | None = None,
443
+ ) -> tuple[AeroStateUnflattened, AeroOutputUnflattened]:
398
444
  r"""
399
- Build the linear state-space system.
445
+ Unravel vectors into their shaped forms and step the system.
400
446
  """
447
+ ref = self.reference
401
448
 
402
- u_ref = AeroInputUnflattened(
403
- zeta_b=self.reference.zeta_b,
404
- zeta_b_dot=self.reference.zeta_b_dot,
405
- nu_b=ArrayList.zeros_like(self.reference.zeta_b)
406
- if self.bound_upwash
407
- else None,
408
- nu_w=ArrayList.zeros_like(self.reference.zeta_w)
409
- if self.wake_upwash
410
- else None,
449
+ def _unravel(vec: Array | None, template: ArrayList) -> ArrayList:
450
+ return (
451
+ ArrayList.from_vector(vect=vec, shapes=template.shape)
452
+ if vec is not None
453
+ else template
454
+ )
455
+
456
+ gamma_b = _unravel(gamma_b_n_vec, ref.gamma_b)
457
+ gamma_w = _unravel(gamma_w_n_vec, ref.gamma_w)
458
+ gamma_b_nm1 = (
459
+ _unravel(gamma_b_nm1_vec, ref.gamma_b) if self.unsteady_force else None
411
460
  )
412
- x_ref = AeroStateUnflattened(
413
- gamma_b=self.reference.gamma_b,
414
- gamma_w=self.reference.gamma_w,
415
- gamma_b_nm1=self.reference.gamma_b if self.unsteady_force else None,
416
- zeta_w=self.reference.zeta_w if self.prescribed_wake else None,
417
- zeta_b=self.reference.zeta_b if self.prescribed_wake else None,
461
+ zeta_b_np1 = _unravel(zeta_b_np1_vec, ref.zeta_b)
462
+ zeta_b_dot_np1 = _unravel(zeta_b_dot_np1_vec, ref.zeta_b_dot)
463
+ zeta_w_n = _unravel(zeta_w_n_vec, ref.zeta_w) if self.prescribed_wake else None
464
+ zeta_b_n = _unravel(zeta_b_n_vec, ref.zeta_b) if self.prescribed_wake else None
465
+ nu_b = (
466
+ _unravel(nu_b_np1_vec, ref.zeta_b)
467
+ if self.bound_upwash and nu_b_np1_vec is not None
468
+ else (ArrayList.zeros_like(ref.zeta_b) if self.bound_upwash else None)
469
+ )
470
+ nu_w = (
471
+ _unravel(nu_w_np1_vec, ref.zeta_w)
472
+ if self.wake_upwash and nu_w_np1_vec is not None
473
+ else (ArrayList.zeros_like(ref.zeta_w) if self.wake_upwash else None)
418
474
  )
419
475
 
420
- u_ref_vec = self._pack_input_vector(u_ref)
421
- x_ref_vec = self._pack_state_vector(x_ref)
476
+ u_np1 = AeroInputUnflattened(
477
+ zeta_b=zeta_b_np1,
478
+ zeta_b_dot=zeta_b_dot_np1,
479
+ nu_b=nu_b,
480
+ nu_w=nu_w,
481
+ )
482
+ x_n = AeroStateUnflattened(
483
+ gamma_b=gamma_b,
484
+ gamma_w=gamma_w,
485
+ gamma_b_nm1=gamma_b_nm1,
486
+ zeta_w=zeta_w_n,
487
+ zeta_b=zeta_b_n,
488
+ )
489
+ return self.step(u_np1=u_np1, x_n=x_n)
490
+
491
+ def gamma_b_step(self, **kwargs: Any) -> Array:
492
+ x_np1, _ = self._step_from_vecs(**kwargs)
493
+ return x_np1.gamma_b.ravel()
494
+
495
+ def wake_prop_step(self, **kwargs: Any) -> tuple[Array | None, Array]:
496
+ x_np1, _ = self._step_from_vecs(**kwargs)
497
+ return (
498
+ x_np1.zeta_w.ravel() if x_np1.zeta_w is not None else None,
499
+ x_np1.gamma_w.ravel(),
500
+ )
501
+
502
+ def f_steady_step(self, **kwargs: Any) -> Array:
503
+ _, y_n = self._step_from_vecs(**kwargs)
504
+ return y_n.f_steady.ravel()
505
+
506
+ def f_unsteady_step(self, **kwargs: Any) -> Array:
507
+ _, y_n = self._step_from_vecs(**kwargs)
508
+ assert y_n.f_unsteady is not None, "unsteady_force must be enabled"
509
+ return y_n.f_unsteady.ravel()
510
+
511
+ def compute_jacobians(
512
+ self,
513
+ input_projection: LinearInputProjection | None = None,
514
+ output_projection: LinearOutputProjection | None = None,
515
+ ) -> dict[str, tuple[Callable[..., Any], dict[str, Any], Sequence[str]]]:
516
+ r"""
517
+ Build the Jacobians for the linear system.
518
+ :param input_projection: If set, projects the inputs onto a different space.
519
+ :param output_projection: If set, projects the forcing outputs onto a different space.
520
+ :return: Mapping of residual name to Jacobian(s).
521
+ """
522
+ ref = self.reference
523
+
524
+ # bound circulation
525
+ gamma_b_args: dict[str, Any] = {
526
+ "gamma_b_n_vec": ref.gamma_b.ravel(),
527
+ "gamma_w_n_vec": ref.gamma_w.ravel(),
528
+ "zeta_b_np1_vec": ref.zeta_b.ravel(),
529
+ "zeta_b_dot_np1_vec": ref.zeta_b_dot.ravel(),
530
+ }
531
+ gamma_b_diff = [
532
+ "gamma_b_n_vec",
533
+ "gamma_w_n_vec",
534
+ "zeta_b_np1_vec",
535
+ "zeta_b_dot_np1_vec",
536
+ ]
537
+ if self.prescribed_wake:
538
+ gamma_b_args["zeta_w_n_vec"] = ref.zeta_w.ravel()
539
+ gamma_b_args["zeta_b_n_vec"] = ref.zeta_b.ravel()
540
+ gamma_b_diff.extend(["zeta_w_n_vec", "zeta_b_n_vec"])
541
+ if self.bound_upwash:
542
+ gamma_b_args["nu_b_np1_vec"] = jnp.zeros(ref.zeta_b.size)
543
+ gamma_b_diff.append("nu_b_np1_vec")
544
+ if self.wake_upwash:
545
+ gamma_b_args["nu_w_np1_vec"] = jnp.zeros(ref.zeta_w.size)
546
+ gamma_b_diff.append("nu_w_np1_vec")
422
547
 
423
- (a, b), (c, d) = jax.jacfwd(self.step_vec, argnums=(0, 1))(x_ref_vec, u_ref_vec)
548
+ # wake propagation (gamma_w and optionally zeta_w)
549
+ wake_args: dict[str, Any] = {
550
+ "gamma_b_n_vec": ref.gamma_b.ravel(),
551
+ "gamma_w_n_vec": ref.gamma_w.ravel(),
552
+ }
553
+ gamma_w_diff = ["gamma_b_n_vec", "gamma_w_n_vec"]
554
+ zeta_w_diff: list[str] = []
555
+ if self.prescribed_wake:
556
+ wake_args["zeta_w_n_vec"] = ref.zeta_w.ravel()
557
+ wake_args["zeta_b_np1_vec"] = ref.zeta_b.ravel()
558
+ zeta_w_diff.extend(["zeta_w_n_vec", "zeta_b_np1_vec"])
559
+ if self.wake_upwash:
560
+ wake_args["nu_w_np1_vec"] = jnp.zeros(ref.zeta_w.size)
561
+ zeta_w_diff.append("nu_w_np1_vec")
562
+ if self.free_wake:
563
+ zeta_w_diff.extend(["gamma_b_n_vec", "gamma_w_n_vec"])
564
+
565
+ # steady forcing
566
+ f_steady_args: dict[str, Any] = {
567
+ "gamma_b_n_vec": ref.gamma_b.ravel(),
568
+ "gamma_w_n_vec": ref.gamma_w.ravel(),
569
+ "zeta_b_np1_vec": ref.zeta_b.ravel(),
570
+ "zeta_b_dot_np1_vec": ref.zeta_b_dot.ravel(),
571
+ }
572
+ f_steady_diff = [
573
+ "gamma_b_n_vec",
574
+ "gamma_w_n_vec",
575
+ "zeta_b_np1_vec",
576
+ "zeta_b_dot_np1_vec",
577
+ ]
578
+ if self.prescribed_wake:
579
+ f_steady_args["zeta_w_n_vec"] = ref.zeta_w.ravel()
580
+ f_steady_diff.append("zeta_w_n_vec")
581
+ if self.bound_upwash:
582
+ f_steady_args["nu_b_np1_vec"] = jnp.zeros(ref.zeta_b.size)
583
+ f_steady_diff.append("nu_b_np1_vec")
584
+
585
+ residuals: dict[
586
+ str, tuple[Callable[..., Any], dict[str, Any], Sequence[str]]
587
+ ] = {
588
+ "gamma_b": (self.gamma_b_step, gamma_b_args, gamma_b_diff),
589
+ "gamma_w": (
590
+ lambda **kw: self.wake_prop_step(**kw)[1],
591
+ wake_args,
592
+ gamma_w_diff,
593
+ ),
594
+ }
595
+ if self.prescribed_wake:
596
+ residuals["zeta_w"] = (
597
+ lambda **kw: self.wake_prop_step(**kw)[0],
598
+ wake_args,
599
+ zeta_w_diff,
600
+ )
601
+ if self.unsteady_force:
602
+ residuals["gamma_b_nm1"] = (
603
+ lambda **kw: kw["gamma_b_n_vec"],
604
+ {"gamma_b_n_vec": ref.gamma_b.ravel()},
605
+ ["gamma_b_n_vec"],
606
+ )
607
+ if self.prescribed_wake:
608
+ # zeta_b state at n+1 == zeta_b input at n+1
609
+ residuals["zeta_b"] = (
610
+ lambda **kw: kw["zeta_b_np1_vec"],
611
+ {"zeta_b_np1_vec": ref.zeta_b.ravel()},
612
+ ["zeta_b_np1_vec"],
613
+ )
614
+ residuals["f_steady"] = (self.f_steady_step, f_steady_args, f_steady_diff)
615
+ if self.unsteady_force:
616
+ f_unsteady_args: dict[str, Any] = {
617
+ "gamma_b_n_vec": ref.gamma_b.ravel(),
618
+ "gamma_b_nm1_vec": ref.gamma_b.ravel(),
619
+ "zeta_b_np1_vec": ref.zeta_b.ravel(),
620
+ }
621
+ residuals["f_unsteady"] = (
622
+ self.f_unsteady_step,
623
+ f_unsteady_args,
624
+ ["gamma_b_n_vec", "gamma_b_nm1_vec", "zeta_b_np1_vec"],
625
+ )
626
+
627
+ # apply I/O projections if provided
628
+ if input_projection is not None:
629
+ residuals = self._apply_input_projection(residuals, input_projection)
630
+ if output_projection is not None:
631
+ residuals = self._apply_output_projection(residuals, output_projection)
632
+
633
+ return residuals
634
+
635
+ @staticmethod
636
+ def _apply_input_projection(
637
+ residuals: dict[str, tuple[Callable[..., Any], dict[str, Any], Sequence[str]]],
638
+ proj: LinearInputProjection,
639
+ ) -> dict[str, tuple[Callable[..., Any], dict[str, Any], Sequence[str]]]:
640
+ r"""
641
+ Project residual functions onto a given input space.
642
+ """
643
+ zeta_args_replaced = {"zeta_b_np1_vec", "zeta_b_dot_np1_vec", "zeta_b_n_vec"}
644
+ proj_names = tuple(proj.arg_names)
645
+
646
+ new_residuals: dict[
647
+ str, tuple[Callable[..., Any], dict[str, Any], Sequence[str]]
648
+ ] = {}
649
+ for name, (func, args, diff_arg_names) in residuals.items():
650
+ if not any(k in args for k in zeta_args_replaced):
651
+ new_residuals[name] = (func, args, diff_arg_names)
652
+ continue
653
+
654
+ new_args = {k: v for k, v in args.items() if k not in zeta_args_replaced}
655
+ for aname, aref in zip(proj_names, proj.arg_refs, strict=True):
656
+ new_args[aname] = aref
657
+
658
+ new_diff = [k for k in diff_arg_names if k not in zeta_args_replaced]
659
+ for aname in proj_names:
660
+ if aname not in new_diff:
661
+ new_diff.append(aname)
662
+
663
+ def _wrap(
664
+ inner: Callable[..., Any],
665
+ needs_zeta_b_np1: bool = "zeta_b_np1_vec" in args,
666
+ needs_zeta_b_dot_np1: bool = "zeta_b_dot_np1_vec" in args,
667
+ needs_zeta_b_n: bool = "zeta_b_n_vec" in args,
668
+ ) -> Callable[..., Any]:
669
+ def wrapped(**kwargs: Any) -> Any:
670
+ proj_kwargs = {n: kwargs.pop(n) for n in proj_names if n in kwargs}
671
+ zb, zbd = proj.to_zeta(**proj_kwargs)
672
+ if needs_zeta_b_np1:
673
+ kwargs["zeta_b_np1_vec"] = zb
674
+ if needs_zeta_b_dot_np1:
675
+ kwargs["zeta_b_dot_np1_vec"] = zbd
676
+ if needs_zeta_b_n:
677
+ # previous-step bound geometry uses the same projection output
678
+ kwargs["zeta_b_n_vec"] = zb
679
+ return inner(**kwargs)
680
+
681
+ return wrapped
682
+
683
+ new_residuals[name] = (_wrap(func), new_args, new_diff)
684
+
685
+ return new_residuals
686
+
687
+ @staticmethod
688
+ def _apply_output_projection(
689
+ residuals: dict[str, tuple[Callable[..., Any], dict[str, Any], Sequence[str]]],
690
+ proj: LinearOutputProjection,
691
+ ) -> dict[str, tuple[Callable[..., Any], dict[str, Any], Sequence[str]]]:
692
+ r"""
693
+ Project forcing outputs into a given output space.
694
+ """
695
+ f_steady_entry = residuals.get("f_steady")
696
+ f_unsteady_entry = residuals.get("f_unsteady")
697
+ assert f_steady_entry is not None
698
+
699
+ f_steady_func, f_steady_args, f_steady_diff = f_steady_entry
700
+ f_unsteady_func: Callable[..., Any] | None
701
+ if f_unsteady_entry is not None:
702
+ f_unsteady_func, f_unsteady_args, f_unsteady_diff = f_unsteady_entry
703
+ else:
704
+ f_unsteady_func = None
705
+ f_unsteady_args = {}
706
+ f_unsteady_diff = []
707
+
708
+ combined_args = {**f_steady_args, **f_unsteady_args}
709
+ combined_diff_ordered: list[str] = []
710
+ for name in list(f_steady_diff) + list(f_unsteady_diff):
711
+ if name not in combined_diff_ordered:
712
+ combined_diff_ordered.append(name)
713
+
714
+ f_steady_kwargs_names = set(f_steady_args.keys())
715
+ f_unsteady_kwargs_names = set(f_unsteady_args.keys())
716
+
717
+ def combined(
718
+ f_unsteady_func_: Callable[..., Any] | None = f_unsteady_func,
719
+ **kwargs: Any,
720
+ ) -> Array:
721
+ f_steady_kwargs = {
722
+ k: v for k, v in kwargs.items() if k in f_steady_kwargs_names
723
+ }
724
+ f_steady = f_steady_func(**f_steady_kwargs)
725
+ if f_unsteady_func_ is not None:
726
+ f_unsteady_kwargs = {
727
+ k: v for k, v in kwargs.items() if k in f_unsteady_kwargs_names
728
+ }
729
+ f_unsteady = f_unsteady_func_(**f_unsteady_kwargs)
730
+ else:
731
+ f_unsteady = None
732
+ return proj.to_output(f_steady, f_unsteady)
733
+
734
+ new_residuals = {
735
+ k: v for k, v in residuals.items() if k not in ("f_steady", "f_unsteady")
736
+ }
737
+ new_residuals[proj.name] = (combined, combined_args, combined_diff_ordered)
738
+
739
+ return new_residuals
740
+
741
+ def create_jacobians(
742
+ self,
743
+ mode: ADMode | dict[str, ADMode] = "reverse",
744
+ batch_size: int | None = None,
745
+ input_projection: LinearInputProjection | None = None,
746
+ output_projection: LinearOutputProjection | None = None,
747
+ ) -> dict[str, dict[str, Array]]:
748
+ r"""
749
+ Compute the per-residual Jacobians using :func:`jacrev_custom`.
750
+ """
751
+ residuals = self.compute_jacobians(
752
+ input_projection=input_projection,
753
+ output_projection=output_projection,
754
+ )
755
+
756
+ jacobians: dict[str, dict[str, Array]] = {}
757
+ for res_name, (res_func, args, diff_arg_names) in residuals.items():
758
+ res_jac_options: dict[str, Callable[..., Array] | None] = {
759
+ arg: None for arg in diff_arg_names
760
+ }
761
+
762
+ res_mode: ADMode = (
763
+ mode.get(res_name, "reverse") if isinstance(mode, dict) else mode
764
+ )
765
+
766
+ jacs, _, _ = jacrev_custom(
767
+ func=res_func,
768
+ jac_options=res_jac_options,
769
+ n_profile_loops=None,
770
+ func_name=res_name,
771
+ map_batch_size=batch_size,
772
+ mode=res_mode,
773
+ )(**args)
774
+ jacobians[res_name] = jacs
775
+
776
+ return jacobians
777
+
778
+ def linearise(
779
+ self,
780
+ batch_size: int | None = None,
781
+ *,
782
+ input_projection: LinearInputProjection | None = None,
783
+ output_projection: LinearOutputProjection | None = None,
784
+ ) -> LinearSystem:
785
+ r"""
786
+ Build the linear state-space system.
787
+ :param batch_size: If not None, batch the Jacobian passes to reduce memory.
788
+ :param input_projection: Optional projection from lower-dim inputs (e.g. beam DOFs) to
789
+ ``(zeta_b, zeta_b_dot)``.
790
+ :param output_projection: Optional projection from ``(f_steady, f_unsteady)`` to a
791
+ smaller output (e.g. beam force); when set, C/D rows correspond to the projected output.
792
+ :return: LinearSystem object.
793
+ """
794
+ jacobians = self.create_jacobians(
795
+ mode="reverse",
796
+ batch_size=batch_size,
797
+ input_projection=input_projection,
798
+ output_projection=output_projection,
799
+ )
800
+
801
+ # (row, column, size)
802
+ ref = self.reference
803
+ state_specs: list[tuple[str, str, int]] = [
804
+ ("gamma_b", "gamma_b_n_vec", ref.gamma_b.size),
805
+ ("gamma_w", "gamma_w_n_vec", ref.gamma_w.size),
806
+ ]
807
+ if self.unsteady_force:
808
+ state_specs.append(("gamma_b_nm1", "gamma_b_nm1_vec", ref.gamma_b.size))
809
+ if self.prescribed_wake:
810
+ state_specs.append(("zeta_w", "zeta_w_n_vec", ref.zeta_w.size))
811
+ state_specs.append(("zeta_b", "zeta_b_n_vec", ref.zeta_b.size))
812
+ state_names = [s[0] for s in state_specs]
813
+ state_arg_names = [s[1] for s in state_specs]
814
+ state_sizes = [s[2] for s in state_specs]
815
+
816
+ if input_projection is None:
817
+ input_specs: list[tuple[str, int]] = [
818
+ ("zeta_b_np1_vec", ref.zeta_b.size),
819
+ ("zeta_b_dot_np1_vec", ref.zeta_b.size),
820
+ ]
821
+ else:
822
+ input_specs = list(
823
+ zip(input_projection.arg_names, input_projection.arg_sizes)
824
+ )
825
+ if self.bound_upwash:
826
+ input_specs.append(("nu_b_np1_vec", ref.zeta_b.size))
827
+ if self.wake_upwash:
828
+ input_specs.append(("nu_w_np1_vec", ref.zeta_w.size))
829
+ input_arg_names = [s[0] for s in input_specs]
830
+ input_sizes = [s[1] for s in input_specs]
831
+
832
+ if output_projection is None:
833
+ n_fs = sum(3 * (m + 1) * (n + 1) for (m, n) in ref.gamma_b.shape)
834
+ output_specs: list[tuple[str, int]] = [("f_steady", n_fs)]
835
+ if self.unsteady_force:
836
+ output_specs.append(("f_unsteady", n_fs))
837
+ else:
838
+ output_specs = [(output_projection.name, output_projection.size)]
839
+ output_names = [s[0] for s in output_specs]
840
+ output_sizes = [s[1] for s in output_specs]
841
+
842
+ a = construct_named_block_jacobian(
843
+ entries=tuple(jacobians[k] for k in state_names),
844
+ keys=state_arg_names,
845
+ widths=state_sizes,
846
+ heights=state_sizes,
847
+ )
848
+ b = construct_named_block_jacobian(
849
+ entries=tuple(jacobians[k] for k in state_names),
850
+ keys=input_arg_names,
851
+ widths=input_sizes,
852
+ heights=state_sizes,
853
+ )
854
+ c = construct_named_block_jacobian(
855
+ entries=tuple(jacobians[k] for k in output_names),
856
+ keys=state_arg_names,
857
+ widths=state_sizes,
858
+ heights=output_sizes,
859
+ )
860
+ d = construct_named_block_jacobian(
861
+ entries=tuple(jacobians[k] for k in output_names),
862
+ keys=input_arg_names,
863
+ widths=input_sizes,
864
+ heights=output_sizes,
865
+ )
424
866
 
425
867
  return LinearSystem(a=a, b=b, c=c, d=d, dt=self.dt)
426
868