From b7daaa692b5b4e19c7937e1a2922714963829f2c Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 22 Dec 2025 01:36:26 +0800 Subject: [PATCH] update weno3 --- .gitignore | 1 + .../weno3/python/02/weno3.py | 576 +++++++++ .../weno3/python/02a/weno3.py | 483 +++++++ .../weno3/python/02b/weno3.py | 489 +++++++ .../weno3/python/02c/weno3.py | 490 +++++++ .../weno3/python/02d/weno3.py | 526 ++++++++ .../weno3/python/02e/weno3.py | 515 ++++++++ .../weno3/python/02f/weno3.py | 549 ++++++++ .../weno3/python/02g/weno3.py | 513 ++++++++ .../weno3/python/02h/weno3.py | 552 ++++++++ .../weno3/python/02i/weno3.py | 584 +++++++++ .../weno3/python/02j/weno3.py | 512 ++++++++ .../weno3/python/03/weno3.py | 610 +++++++++ .../weno3/python/03a/weno3.py | 625 +++++++++ .../weno3/python/03b/weno3.py | 630 +++++++++ .../weno3/python/03c/weno3.py | 616 +++++++++ .../weno3/python/03d/weno3.py | 613 +++++++++ .../weno3/python/03e/weno3.py | 613 +++++++++ .../weno3/python/03f/weno3.py | 614 +++++++++ .../weno3/python/03g/weno3.py | 645 ++++++++++ .../weno3/python/03h/weno3.py | 695 ++++++++++ .../weno3/python/03i/weno3.py | 738 +++++++++++ .../weno3/python/04/weno3.py | 795 ++++++++++++ .../weno3/python/04a/core.py | 737 +++++++++++ .../weno3/python/04a/plotter.py | 107 ++ .../weno3/python/04a/run_eno_weno.py | 50 + .../weno3/python/04b/core.py | 810 ++++++++++++ .../weno3/python/04b/plotter.py | 107 ++ .../weno3/python/04b/run_eno_weno.py | 50 + .../weno3/python/04c/core.py | 752 +++++++++++ .../weno3/python/04c/flux.py | 61 + .../weno3/python/04c/plotter.py | 107 ++ .../weno3/python/04c/run_eno_weno.py | 50 + .../weno3/python/04d/boundary.py | 83 ++ .../weno3/python/04d/core.py | 673 ++++++++++ .../weno3/python/04d/flux.py | 61 + .../weno3/python/04d/plotter.py | 107 ++ .../weno3/python/04d/run_eno_weno.py | 50 + .../weno3/python/04e/boundary.py | 83 ++ .../weno3/python/04e/core.py | 563 +++++++++ .../weno3/python/04e/flux.py | 61 + .../weno3/python/04e/plotter.py | 107 ++ .../weno3/python/04e/run_eno_weno.py | 50 + .../weno3/python/04e/time_integration.py | 111 ++ .../weno3/python/04f/boundary.py | 83 ++ .../weno3/python/04f/core.py | 543 ++++++++ .../weno3/python/04f/flux.py | 61 + .../weno3/python/04f/mesh.py | 26 + .../weno3/python/04f/plotter.py | 107 ++ .../weno3/python/04f/run_eno_weno.py | 50 + .../weno3/python/04f/time_integration.py | 111 ++ .../weno3/python/04g/boundary.py | 83 ++ .../weno3/python/04g/core.py | 333 +++++ .../weno3/python/04g/flux.py | 61 + .../weno3/python/04g/mesh.py | 26 + .../weno3/python/04g/plotter.py | 107 ++ .../weno3/python/04g/reconstructor.py | 166 +++ .../weno3/python/04g/run_eno_weno.py | 50 + .../weno3/python/04g/time_integration.py | 111 ++ .../weno3/python/04h/boundary.py | 83 ++ .../weno3/python/04h/core.py | 259 ++++ .../weno3/python/04h/flux.py | 61 + .../weno3/python/04h/initial_condition.py | 81 ++ .../weno3/python/04h/mesh.py | 26 + .../weno3/python/04h/plotter.py | 107 ++ .../weno3/python/04h/reconstructor.py | 166 +++ .../weno3/python/04h/run_eno_weno.py | 50 + .../weno3/python/04h/time_integration.py | 111 ++ .../weno3/python/04i/boundary.py | 83 ++ .../weno3/python/04i/core.py | 208 +++ .../weno3/python/04i/domain.py | 55 + .../weno3/python/04i/flux.py | 61 + .../weno3/python/04i/initial_condition.py | 81 ++ .../weno3/python/04i/mesh.py | 26 + .../weno3/python/04i/plotter.py | 107 ++ .../weno3/python/04i/reconstructor.py | 166 +++ .../weno3/python/04i/run_eno_weno.py | 50 + .../weno3/python/04i/time_integration.py | 111 ++ .../weno3/python/04j/boundary.py | 83 ++ .../weno3/python/04j/core.py | 169 +++ .../weno3/python/04j/domain.py | 55 + .../weno3/python/04j/flux.py | 61 + .../weno3/python/04j/initial_condition.py | 81 ++ .../weno3/python/04j/mesh.py | 26 + .../weno3/python/04j/plotter.py | 107 ++ .../weno3/python/04j/reconstructor.py | 166 +++ .../weno3/python/04j/run_eno_weno.py | 50 + .../weno3/python/04j/solution.py | 40 + .../weno3/python/04j/time_integration.py | 111 ++ .../weno3/python/04k/boundary.py | 83 ++ .../weno3/python/04k/config.py | 39 + .../weno3/python/04k/domain.py | 55 + .../weno3/python/04k/flux.py | 61 + .../weno3/python/04k/initial_condition.py | 81 ++ .../weno3/python/04k/mesh.py | 26 + .../weno3/python/04k/plotter.py | 107 ++ .../weno3/python/04k/reconstructor.py | 166 +++ .../weno3/python/04k/residual.py | 40 + .../weno3/python/04k/run_eno_weno.py | 52 + .../weno3/python/04k/solution.py | 40 + .../weno3/python/04k/solver.py | 89 ++ .../weno3/python/04k/time_integration.py | 111 ++ .../weno3/python/04l/boundary.py | 83 ++ .../weno3/python/04l/config.py | 39 + .../weno3/python/04l/domain.py | 55 + .../weno3/python/04l/flux.py | 61 + .../weno3/python/04l/initial_condition.py | 81 ++ .../weno3/python/04l/mesh.py | 26 + .../weno3/python/04l/plotter.py | 107 ++ .../weno3/python/04l/reconstructor.py | 166 +++ .../weno3/python/04l/residual.py | 40 + .../weno3/python/04l/run_eno_weno.py | 52 + .../weno3/python/04l/solution.py | 40 + .../weno3/python/04l/solver.py | 89 ++ .../weno3/python/04l/time_integration.py | 111 ++ .../weno3/python/04m/boundary.py | 83 ++ .../weno3/python/04m/config.py | 41 + .../weno3/python/04m/domain.py | 56 + .../weno3/python/04m/flux.py | 61 + .../weno3/python/04m/initial_condition.py | 81 ++ .../weno3/python/04m/mesh.py | 26 + .../weno3/python/04m/plotter.py | 107 ++ .../python/04m/reconstructor/__init__.py | 4 + .../weno3/python/04m/reconstructor/base.py | 7 + .../weno3/python/04m/reconstructor/eno.py | 88 ++ .../weno3/python/04m/reconstructor/factory.py | 35 + .../weno3/python/04m/reconstructor/weno3.py | 56 + .../weno3/python/04m/residual.py | 40 + .../weno3/python/04m/run_eno_weno.py | 52 + .../weno3/python/04m/solution.py | 40 + .../weno3/python/04m/solver.py | 90 ++ .../weno3/python/04m/time_integration.py | 111 ++ example/figure/1d/03u/cfd.png | Bin 32092 -> 0 bytes example/figure/1d/04c/cfd.png | Bin 63137 -> 0 bytes example/figure/1d/04e1/testprj.py | 132 ++ example/figure/1d/05/testprj.py | 114 ++ example/figure/1d/05a/testprj.py | 122 ++ example/figure/1d/05b/testprj.py | 107 ++ example/figure/1d/05c/testprj.py | 108 ++ example/figure/1d/05d/testprj.py | 106 ++ example/figure/1d/05e/testprj.py | 106 ++ example/figure/1d/05f/testprj.py | 108 ++ example/figure/1d/05g/testprj.py | 105 ++ example/figure/1d/05h/testprj.py | 105 ++ example/figure/1d/05i/testprj.py | 110 ++ example/figure/1d/05j/testprj.py | 131 ++ example/figure/1d/Simple1DGrid/01/testprj.py | 88 ++ example/figure/1d/animation/01/animation.py | 28 + example/figure/1d/animation/01a/animation.py | 33 + example/figure/1d/animation/01b/animation.py | 36 + example/figure/1d/animation/01c/animation.py | 52 + example/figure/1d/animation/01d/animation.py | 35 + example/figure/1d/arrow/01/testprj.py | 18 + example/figure/1d/arrow/01a/testprj.py | 22 + example/figure/1d/arrow/01b/testprj.py | 26 + example/figure/1d/arrow/01c/testprj.py | 38 + example/figure/1d/arrow/01d/testprj.py | 97 ++ example/figure/1d/arrow/01e/testprj.py | 266 ++++ example/figure/1d/arrow/01f/testprj.py | 120 ++ example/figure/1d/arrow/01f0/testprj.py | 110 ++ example/figure/1d/arrow/01f1/testprj.py | 73 ++ example/figure/1d/arrow/01g/testprj.py | 279 ++++ example/figure/1d/arrow/01h/testprj.py | 335 +++++ example/figure/1d/eno/01/cfd.png | Bin 24534 -> 0 bytes example/figure/1d/eno/01a/cfd.png | Bin 27153 -> 0 bytes example/figure/1d/eno/01b/cfd.png | Bin 34422 -> 0 bytes example/figure/1d/eno/01c/cfd.png | Bin 36535 -> 0 bytes example/figure/1d/eno/01e/cfd.png | Bin 36158 -> 0 bytes example/figure/1d/eno/01f/cfd.png | Bin 36295 -> 0 bytes example/figure/1d/eno/02e/cfd.png | Bin 35387 -> 0 bytes example/figure/1d/eno/02g/cfd.png | Bin 47257 -> 0 bytes example/figure/1d/eno/02h/cfd.png | Bin 37308 -> 0 bytes example/figure/1d/eno/03/testprj.py | 207 +++ example/figure/1d/eno/03a/testprj.py | 218 ++++ example/figure/1d/eno/03b/testprj.py | 100 ++ example/figure/1d/eno/03c/testprj.py | 97 ++ example/figure/1d/eno/03d/testprj.py | 95 ++ example/figure/1d/eno/03e/testprj.py | 116 ++ example/figure/1d/eno/03f/testprj.py | 120 ++ .../figure/1d/finite_difference/01/testprj.py | 500 ++++++++ .../1d/finite_difference/01a/testprj.py | 614 +++++++++ .../1d/finite_difference/01b/testprj.py | 600 +++++++++ .../1d/finite_difference/01c/testprj.py | 646 ++++++++++ .../1d/finite_difference/01d/testprj.py | 636 ++++++++++ .../1d/finite_difference/01e/testprj.py | 636 ++++++++++ example/figure/1d/mesh/01/00_testanimation.py | 28 + .../figure/1d/mesh/01/01_cfd_grid_storage.py | 214 ++++ .../1d/mesh/01/02_convection_schemes.py | 126 ++ .../1d/mesh/01/03_interpolation_methods.py | 119 ++ example/figure/1d/mesh/01/04_cfd_animation.py | 372 ++++++ .../figure/1d/mesh/01/04_cfd_animationBAK.py | 299 +++++ .../figure/1d/mesh/01/04_cfd_animationOld.py | 299 +++++ .../1d/mesh/01/05_interactive_cfd_plot.py | 574 +++++++++ example/figure/1d/mesh/01/testprj.py | 140 ++ .../figure/1d/mesh/01a/00_testanimation.py | 28 + .../figure/1d/mesh/01a/01_cfd_grid_storage.py | 214 ++++ .../1d/mesh/01a/02_convection_schemes.py | 126 ++ .../1d/mesh/01a/03_interpolation_methods.py | 119 ++ .../figure/1d/mesh/01a/04_cfd_animation.py | 178 +++ .../1d/mesh/01a/05_interactive_cfd_plot.py | 83 ++ example/figure/1d/mesh/01a/testprj.py | 140 ++ example/figure/1d/mesh/02/testprj.py | 21 + example/figure/1d/mesh/02a/testprj.py | 32 + example/figure/1d/mesh/03/testprj.py | 20 + example/figure/1d/mesh/03a/testprj.py | 27 + example/figure/1d/mesh/03b/testprj.py | 20 + example/figure/1d/mesh/03c/testprj.py | 34 + example/figure/1d/mesh/03d/testprj.py | 44 + example/figure/1d/mesh/03e/testprj.py | 30 + example/figure/1d/mesh/04/testprj.py | 36 + example/figure/1d/mesh/04a/testprj.py | 45 + example/figure/1d/mesh/04b/testprj.py | 48 + example/figure/1d/mesh/04c/testprj.py | 67 + example/figure/1d/mesh/04d/testprj.py | 68 + example/figure/1d/mesh/04e/testprj.py | 27 + example/figure/1d/mesh/04f/testprj.py | 104 ++ example/figure/1d/mesh/05/testprj.py | 171 +++ example/figure/1d/mesh/05a/testprj.py | 186 +++ example/figure/1d/mesh/05b/testprj.py | 237 ++++ example/figure/1d/mesh/05c/testprj.py | 302 +++++ example/figure/1d/mesh/05d/testprj.py | 363 ++++++ example/figure/1d/mesh/05e/testprj.py | 362 ++++++ example/figure/1d/mesh/05f/testprj.py | 375 ++++++ example/figure/1d/mesh/05g/testprj.py | 378 ++++++ example/figure/1d/periodic/01/testprj.py | 375 ++++++ example/figure/1d/periodic/01a/testprj.py | 396 ++++++ example/figure/1d/periodic/01b/testprj.py | 418 ++++++ example/figure/1d/periodic/01c/testprj.py | 424 +++++++ example/figure/1d/periodic/01d/testprj.py | 464 +++++++ example/figure/1d/periodic/01e/testprj.py | 480 +++++++ example/figure/1d/periodic/01f/testprj.py | 489 +++++++ example/figure/1d/periodic/01g/testprj.py | 491 +++++++ example/figure/1d/periodic/01g0/testprj.py | 490 +++++++ example/figure/1d/weno/LinearWeights/01/xi.py | 542 ++++++++ .../1d/weno/interplate/0st/01/testprj.py | 120 ++ .../1d/weno/interplate/0st/01a/testprj.py | 72 ++ .../1d/weno/interplate/0st/01b/testprj.py | 95 ++ .../1d/weno/interplate/0st/01c/testprj.py | 92 ++ .../1d/weno/interplate/0st/01d/testprj.py | 101 ++ .../compute_integral/01/compute_integral.py | 271 ++++ .../compute_integral/01a/compute_integral.py | 115 ++ .../compute_integral/01b/compute_integral.py | 121 ++ .../compute_integral/01c/compute_integral.py | 173 +++ .../compute_integral/01d/compute_integral.py | 136 ++ .../compute_integral/01e/compute_integral.py | 141 +++ .../compute_integral/02/compute_integral.py | 299 +++++ .../compute_integral/02a/compute_integral.py | 263 ++++ .../compute_integral/02b/compute_integral.py | 344 +++++ .../compute_integral/02c/compute_integral.py | 427 +++++++ .../compute_integral/02d/compute_integral.py | 388 ++++++ .../compute_integral/02e/compute_integral.py | 482 +++++++ .../compute_integral/02f/compute_integral.py | 541 ++++++++ .../compute_integral/03/compute_integral.py | 544 ++++++++ .../1d/weno/interplate/counter/01/counter.py | 20 + .../max_common_factor/01/max_common_factor.py | 90 ++ .../01/polynomial_operations.py | 225 ++++ .../01a/polynomial_operations.py | 263 ++++ .../01b/polynomial_operations.py | 441 +++++++ .../01c/polynomial_operations.py | 436 +++++++ .../01d/polynomial_operations.py | 599 +++++++++ .../01e/polynomial_operations.py | 564 +++++++++ .../02/polynomial_operations.py | 598 +++++++++ .../02a/polynomial_operations.py | 708 +++++++++++ .../02b/polynomial_operations.py | 717 +++++++++++ .../02c/polynomial_operations.py | 796 ++++++++++++ .../02d/polynomial_operations.py | 805 ++++++++++++ .../02e/polynomial_operations.py | 859 +++++++++++++ .../02f/polynomial_operations.py | 870 +++++++++++++ .../03/polynomial_operations.py | 1124 +++++++++++++++++ .../03a/polynomial_operations.py | 1055 ++++++++++++++++ .../03b/polynomial_operations.py | 893 +++++++++++++ .../03c/polynomial_operations.py | 906 +++++++++++++ .../03d/polynomial_operations.py | 911 +++++++++++++ .../03e/polynomial_operations.py | 912 +++++++++++++ .../01/smoothness_indicator.py | 591 +++++++++ .../01a/smoothness_indicator.py | 623 +++++++++ .../01b/smoothness_indicator.py | 799 ++++++++++++ .../01/weno5_smoothness_sympy.py | 56 + example/figure/1d/weno/interplate/xi/01/xi.py | 12 + .../figure/1d/weno/interplate/xi/01a/xi.py | 16 + example/figure/1d/weno/interplate/xi/02/xi.py | 12 + .../figure/1d/weno/interplate/xi/02a/xi.py | 12 + .../figure/1d/weno/interplate/xi/02b/xi.py | 25 + .../figure/1d/weno/interplate/xi/02c/xi.py | 26 + .../figure/1d/weno/interplate/xi/02d/xi.py | 45 + .../figure/1d/weno/interplate/xi/02e/xi.py | 17 + .../figure/1d/weno/interplate/xi/02f/xi.py | 24 + .../figure/1d/weno/interplate/xi/02g/xi.py | 76 ++ example/figure/1d/weno/interplate/xi/03/xi.py | 18 + .../figure/1d/weno/interplate/xi/03a/xi.py | 28 + .../figure/1d/weno/interplate/xi/03b/xi.py | 88 ++ .../figure/1d/weno/interplate/xi/03c/xi.py | 128 ++ .../figure/1d/weno/interplate/xi/03d/xi.py | 169 +++ .../figure/1d/weno/interplate/xi/03e/xi.py | 173 +++ example/figure/1d/weno/interplate/xi/04/xi.py | 212 ++++ .../figure/1d/weno/interplate/xi/04a/xi.py | 209 +++ example/figure/1d/weno/interplate/xi/05/xi.py | 192 +++ .../figure/1d/weno/interplate/xi/05a/xi.py | 205 +++ .../figure/1d/weno/interplate/xi/05b/xi.py | 229 ++++ .../figure/1d/weno/interplate/xi/05c/xi.py | 246 ++++ .../figure/1d/weno/interplate/xi/05d/xi.py | 269 ++++ .../figure/1d/weno/interplate/xi/05e/xi.py | 281 +++++ .../figure/1d/weno/interplate/xi/05f/xi.py | 284 +++++ .../figure/1d/weno/interplate/xi/05g/xi.py | 310 +++++ example/figure/1d/weno/interplate/xi/06/xi.py | 321 +++++ .../figure/1d/weno/interplate/xi/06a/xi.py | 249 ++++ .../figure/1d/weno/interplate/xi/06b/xi.py | 223 ++++ .../figure/1d/weno/interplate/xi/06c/xi.py | 178 +++ example/figure/1d/weno/interplate/xi/07/xi.py | 195 +++ .../figure/1d/weno/interplate/xi/07a/xi.py | 200 +++ .../figure/1d/weno/interplate/xi/07b/xi.py | 211 ++++ .../figure/1d/weno/interplate/xi/07c/xi.py | 270 ++++ .../figure/1d/weno/interplate/xi/07d/xi.py | 264 ++++ .../figure/1d/weno/interplate/xi/07e/xi.py | 267 ++++ .../figure/1d/weno/interplate/xi/07f/xi.py | 274 ++++ .../figure/1d/weno/interplate/xi/07g/xi.py | 326 +++++ .../figure/1d/weno/interplate/xi/07h/xi.py | 338 +++++ .../figure/1d/weno/interplate/xi/07i/xi.py | 318 +++++ .../figure/1d/weno/interplate/xi/07j/xi.py | 374 ++++++ example/figure/1d/weno/interplate/xi/08/xi.py | 392 ++++++ .../figure/1d/weno/interplate/xi/08a/xi.py | 440 +++++++ .../figure/1d/weno/interplate/xi/08b/xi.py | 506 ++++++++ .../figure/1d/weno/interplate/xi/08c/xi.py | 502 ++++++++ .../figure/1d/weno/interplate/xi/08d/xi.py | 507 ++++++++ .../figure/1d/weno/interplate/xi/08e/xi.py | 516 ++++++++ .../figure/1d/weno/interplate/xi/08f/xi.py | 510 ++++++++ .../figure/1d/weno/interplate/xi/08g/xi.py | 520 ++++++++ example/figure/1d/weno/interplate/xi/09/xi.py | 521 ++++++++ .../figure/1d/weno/interplate/xi/09a/xi.py | 569 +++++++++ .../figure/1d/weno/interplate/xi/09b/xi.py | 542 ++++++++ example/figure/1d/weno/matrix/01/matrix.py | 54 + example/figure/1d/weno/matrix/01a/matrix.py | 28 + example/figure/1d/weno/matrix/01b/matrix.py | 64 + example/figure/1d/weno/matrix/01c/matrix.py | 72 ++ example/figure/1d/weno/matrix/02/matrix.py | 87 ++ example/figure/1d/weno/matrix/02a/matrix.py | 89 ++ example/figure/1d/weno/matrix/02b/matrix.py | 89 ++ example/figure/1d/weno/matrix/02c/matrix.py | 123 ++ example/figure/1d/weno/matrix/02d/matrix.py | 91 ++ example/figure/1d/weno/matrix/02e/matrix.py | 219 ++++ example/figure/1d/weno/matrix/02f/matrix.py | 190 +++ example/figure/1d/weno/matrix/02g/matrix.py | 264 ++++ example/figure/1d/weno/matrix/02h/matrix.py | 289 +++++ example/figure/1d/weno/matrix/03/matrix.py | 352 ++++++ example/figure/1d/weno/matrix/03a/matrix.py | 254 ++++ .../smoothness/01/polynomial_operations.py | 913 +++++++++++++ .../1d/weno/some_help_code/01/test_sorting.py | 55 + .../some_help_code/01a/weno_debug_sort.py | 170 +++ .../some_help_code/01b/weno_debug_sort.py | 173 +++ .../1d/weno/some_help_code/02/testprj.py | 31 + .../1d/weno/some_help_code/02a/testprj.py | 60 + .../1d/weno/some_help_code/02aa/testprj.py | 76 ++ .../1d/weno/some_help_code/02b/testprj.py | 296 +++++ .../1d/weno/some_help_code/02c/testprj.py | 21 + .../1d/weno/some_help_code/02d/formulas.json | 5 + .../1d/weno/some_help_code/02d/testprj.py | 33 + .../1d/weno/some_help_code/02e/formulas.json | 5 + .../1d/weno/some_help_code/02e/testprj.py | 48 + .../1d/weno/some_help_code/03/testprj.py | 87 ++ .../1d/weno/some_help_code/03a/testprj.py | 108 ++ .../1d/weno/some_help_code/03b/testprj.py | 220 ++++ .../1d/weno/some_help_code/03c/testprj.py | 105 ++ .../1d/weno/some_help_code/04/testprj.py | 41 + .../1d/weno/some_help_code/04a/testprj.py | 58 + .../1d/weno/some_help_code/04b/testprj.py | 97 ++ .../1d/weno/some_help_code/04c/testprj.py | 108 ++ .../1d/weno/some_help_code/04d/testprj.py | 119 ++ .../1d/weno/some_help_code/05/testprj.py | 106 ++ .../1d/weno/some_help_code/05a/testprj.py | 76 ++ .../figure/1d/weno/wenoinfo/01/wenoinfo.py | 1016 +++++++++++++++ example/weno-coef/crj/python/01e/crj.py | 48 + example/weno-coef/crj/python/01f/crj.py | 52 + example/weno-coef/crj/python/01g/crj.py | 53 + .../crj/python/expand_formula/01/testprj.py | 44 + .../crj/python/expand_formula/01a/testprj.py | 44 + .../crj/python/expand_formula/01b/testprj.py | 141 +++ .../crj/python/expand_formula/01b0/testprj.py | 166 +++ .../crj/python/expand_formula/01c/testprj.py | 180 +++ .../crj/python/expand_formula/01d/testprj.py | 173 +++ .../crj/python/expand_formula/01e/testprj.py | 175 +++ .../crj/python/expand_formula/01f/testprj.py | 171 +++ .../crj/python/expand_formula/01f0/testprj.py | 147 +++ .../crj/python/expand_formula/01g/testprj.py | 167 +++ .../expand_formula/KKKKKK01f/testprj.py | 210 +++ 384 files changed, 85109 insertions(+) create mode 100644 example/1d-linear-convection/weno3/python/02/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/02a/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/02b/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/02c/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/02d/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/02e/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/02f/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/02g/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/02h/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/02i/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/02j/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/03/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/03a/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/03b/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/03c/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/03d/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/03e/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/03f/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/03g/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/03h/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/03i/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/04/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/04a/core.py create mode 100644 example/1d-linear-convection/weno3/python/04a/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04a/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04b/core.py create mode 100644 example/1d-linear-convection/weno3/python/04b/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04b/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04c/core.py create mode 100644 example/1d-linear-convection/weno3/python/04c/flux.py create mode 100644 example/1d-linear-convection/weno3/python/04c/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04c/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04d/boundary.py create mode 100644 example/1d-linear-convection/weno3/python/04d/core.py create mode 100644 example/1d-linear-convection/weno3/python/04d/flux.py create mode 100644 example/1d-linear-convection/weno3/python/04d/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04d/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04e/boundary.py create mode 100644 example/1d-linear-convection/weno3/python/04e/core.py create mode 100644 example/1d-linear-convection/weno3/python/04e/flux.py create mode 100644 example/1d-linear-convection/weno3/python/04e/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04e/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04e/time_integration.py create mode 100644 example/1d-linear-convection/weno3/python/04f/boundary.py create mode 100644 example/1d-linear-convection/weno3/python/04f/core.py create mode 100644 example/1d-linear-convection/weno3/python/04f/flux.py create mode 100644 example/1d-linear-convection/weno3/python/04f/mesh.py create mode 100644 example/1d-linear-convection/weno3/python/04f/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04f/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04f/time_integration.py create mode 100644 example/1d-linear-convection/weno3/python/04g/boundary.py create mode 100644 example/1d-linear-convection/weno3/python/04g/core.py create mode 100644 example/1d-linear-convection/weno3/python/04g/flux.py create mode 100644 example/1d-linear-convection/weno3/python/04g/mesh.py create mode 100644 example/1d-linear-convection/weno3/python/04g/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04g/reconstructor.py create mode 100644 example/1d-linear-convection/weno3/python/04g/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04g/time_integration.py create mode 100644 example/1d-linear-convection/weno3/python/04h/boundary.py create mode 100644 example/1d-linear-convection/weno3/python/04h/core.py create mode 100644 example/1d-linear-convection/weno3/python/04h/flux.py create mode 100644 example/1d-linear-convection/weno3/python/04h/initial_condition.py create mode 100644 example/1d-linear-convection/weno3/python/04h/mesh.py create mode 100644 example/1d-linear-convection/weno3/python/04h/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04h/reconstructor.py create mode 100644 example/1d-linear-convection/weno3/python/04h/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04h/time_integration.py create mode 100644 example/1d-linear-convection/weno3/python/04i/boundary.py create mode 100644 example/1d-linear-convection/weno3/python/04i/core.py create mode 100644 example/1d-linear-convection/weno3/python/04i/domain.py create mode 100644 example/1d-linear-convection/weno3/python/04i/flux.py create mode 100644 example/1d-linear-convection/weno3/python/04i/initial_condition.py create mode 100644 example/1d-linear-convection/weno3/python/04i/mesh.py create mode 100644 example/1d-linear-convection/weno3/python/04i/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04i/reconstructor.py create mode 100644 example/1d-linear-convection/weno3/python/04i/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04i/time_integration.py create mode 100644 example/1d-linear-convection/weno3/python/04j/boundary.py create mode 100644 example/1d-linear-convection/weno3/python/04j/core.py create mode 100644 example/1d-linear-convection/weno3/python/04j/domain.py create mode 100644 example/1d-linear-convection/weno3/python/04j/flux.py create mode 100644 example/1d-linear-convection/weno3/python/04j/initial_condition.py create mode 100644 example/1d-linear-convection/weno3/python/04j/mesh.py create mode 100644 example/1d-linear-convection/weno3/python/04j/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04j/reconstructor.py create mode 100644 example/1d-linear-convection/weno3/python/04j/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04j/solution.py create mode 100644 example/1d-linear-convection/weno3/python/04j/time_integration.py create mode 100644 example/1d-linear-convection/weno3/python/04k/boundary.py create mode 100644 example/1d-linear-convection/weno3/python/04k/config.py create mode 100644 example/1d-linear-convection/weno3/python/04k/domain.py create mode 100644 example/1d-linear-convection/weno3/python/04k/flux.py create mode 100644 example/1d-linear-convection/weno3/python/04k/initial_condition.py create mode 100644 example/1d-linear-convection/weno3/python/04k/mesh.py create mode 100644 example/1d-linear-convection/weno3/python/04k/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04k/reconstructor.py create mode 100644 example/1d-linear-convection/weno3/python/04k/residual.py create mode 100644 example/1d-linear-convection/weno3/python/04k/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04k/solution.py create mode 100644 example/1d-linear-convection/weno3/python/04k/solver.py create mode 100644 example/1d-linear-convection/weno3/python/04k/time_integration.py create mode 100644 example/1d-linear-convection/weno3/python/04l/boundary.py create mode 100644 example/1d-linear-convection/weno3/python/04l/config.py create mode 100644 example/1d-linear-convection/weno3/python/04l/domain.py create mode 100644 example/1d-linear-convection/weno3/python/04l/flux.py create mode 100644 example/1d-linear-convection/weno3/python/04l/initial_condition.py create mode 100644 example/1d-linear-convection/weno3/python/04l/mesh.py create mode 100644 example/1d-linear-convection/weno3/python/04l/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04l/reconstructor.py create mode 100644 example/1d-linear-convection/weno3/python/04l/residual.py create mode 100644 example/1d-linear-convection/weno3/python/04l/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04l/solution.py create mode 100644 example/1d-linear-convection/weno3/python/04l/solver.py create mode 100644 example/1d-linear-convection/weno3/python/04l/time_integration.py create mode 100644 example/1d-linear-convection/weno3/python/04m/boundary.py create mode 100644 example/1d-linear-convection/weno3/python/04m/config.py create mode 100644 example/1d-linear-convection/weno3/python/04m/domain.py create mode 100644 example/1d-linear-convection/weno3/python/04m/flux.py create mode 100644 example/1d-linear-convection/weno3/python/04m/initial_condition.py create mode 100644 example/1d-linear-convection/weno3/python/04m/mesh.py create mode 100644 example/1d-linear-convection/weno3/python/04m/plotter.py create mode 100644 example/1d-linear-convection/weno3/python/04m/reconstructor/__init__.py create mode 100644 example/1d-linear-convection/weno3/python/04m/reconstructor/base.py create mode 100644 example/1d-linear-convection/weno3/python/04m/reconstructor/eno.py create mode 100644 example/1d-linear-convection/weno3/python/04m/reconstructor/factory.py create mode 100644 example/1d-linear-convection/weno3/python/04m/reconstructor/weno3.py create mode 100644 example/1d-linear-convection/weno3/python/04m/residual.py create mode 100644 example/1d-linear-convection/weno3/python/04m/run_eno_weno.py create mode 100644 example/1d-linear-convection/weno3/python/04m/solution.py create mode 100644 example/1d-linear-convection/weno3/python/04m/solver.py create mode 100644 example/1d-linear-convection/weno3/python/04m/time_integration.py delete mode 100644 example/figure/1d/03u/cfd.png delete mode 100644 example/figure/1d/04c/cfd.png create mode 100644 example/figure/1d/04e1/testprj.py create mode 100644 example/figure/1d/05/testprj.py create mode 100644 example/figure/1d/05a/testprj.py create mode 100644 example/figure/1d/05b/testprj.py create mode 100644 example/figure/1d/05c/testprj.py create mode 100644 example/figure/1d/05d/testprj.py create mode 100644 example/figure/1d/05e/testprj.py create mode 100644 example/figure/1d/05f/testprj.py create mode 100644 example/figure/1d/05g/testprj.py create mode 100644 example/figure/1d/05h/testprj.py create mode 100644 example/figure/1d/05i/testprj.py create mode 100644 example/figure/1d/05j/testprj.py create mode 100644 example/figure/1d/Simple1DGrid/01/testprj.py create mode 100644 example/figure/1d/animation/01/animation.py create mode 100644 example/figure/1d/animation/01a/animation.py create mode 100644 example/figure/1d/animation/01b/animation.py create mode 100644 example/figure/1d/animation/01c/animation.py create mode 100644 example/figure/1d/animation/01d/animation.py create mode 100644 example/figure/1d/arrow/01/testprj.py create mode 100644 example/figure/1d/arrow/01a/testprj.py create mode 100644 example/figure/1d/arrow/01b/testprj.py create mode 100644 example/figure/1d/arrow/01c/testprj.py create mode 100644 example/figure/1d/arrow/01d/testprj.py create mode 100644 example/figure/1d/arrow/01e/testprj.py create mode 100644 example/figure/1d/arrow/01f/testprj.py create mode 100644 example/figure/1d/arrow/01f0/testprj.py create mode 100644 example/figure/1d/arrow/01f1/testprj.py create mode 100644 example/figure/1d/arrow/01g/testprj.py create mode 100644 example/figure/1d/arrow/01h/testprj.py delete mode 100644 example/figure/1d/eno/01/cfd.png delete mode 100644 example/figure/1d/eno/01a/cfd.png delete mode 100644 example/figure/1d/eno/01b/cfd.png delete mode 100644 example/figure/1d/eno/01c/cfd.png delete mode 100644 example/figure/1d/eno/01e/cfd.png delete mode 100644 example/figure/1d/eno/01f/cfd.png delete mode 100644 example/figure/1d/eno/02e/cfd.png delete mode 100644 example/figure/1d/eno/02g/cfd.png delete mode 100644 example/figure/1d/eno/02h/cfd.png create mode 100644 example/figure/1d/eno/03/testprj.py create mode 100644 example/figure/1d/eno/03a/testprj.py create mode 100644 example/figure/1d/eno/03b/testprj.py create mode 100644 example/figure/1d/eno/03c/testprj.py create mode 100644 example/figure/1d/eno/03d/testprj.py create mode 100644 example/figure/1d/eno/03e/testprj.py create mode 100644 example/figure/1d/eno/03f/testprj.py create mode 100644 example/figure/1d/finite_difference/01/testprj.py create mode 100644 example/figure/1d/finite_difference/01a/testprj.py create mode 100644 example/figure/1d/finite_difference/01b/testprj.py create mode 100644 example/figure/1d/finite_difference/01c/testprj.py create mode 100644 example/figure/1d/finite_difference/01d/testprj.py create mode 100644 example/figure/1d/finite_difference/01e/testprj.py create mode 100644 example/figure/1d/mesh/01/00_testanimation.py create mode 100644 example/figure/1d/mesh/01/01_cfd_grid_storage.py create mode 100644 example/figure/1d/mesh/01/02_convection_schemes.py create mode 100644 example/figure/1d/mesh/01/03_interpolation_methods.py create mode 100644 example/figure/1d/mesh/01/04_cfd_animation.py create mode 100644 example/figure/1d/mesh/01/04_cfd_animationBAK.py create mode 100644 example/figure/1d/mesh/01/04_cfd_animationOld.py create mode 100644 example/figure/1d/mesh/01/05_interactive_cfd_plot.py create mode 100644 example/figure/1d/mesh/01/testprj.py create mode 100644 example/figure/1d/mesh/01a/00_testanimation.py create mode 100644 example/figure/1d/mesh/01a/01_cfd_grid_storage.py create mode 100644 example/figure/1d/mesh/01a/02_convection_schemes.py create mode 100644 example/figure/1d/mesh/01a/03_interpolation_methods.py create mode 100644 example/figure/1d/mesh/01a/04_cfd_animation.py create mode 100644 example/figure/1d/mesh/01a/05_interactive_cfd_plot.py create mode 100644 example/figure/1d/mesh/01a/testprj.py create mode 100644 example/figure/1d/mesh/02/testprj.py create mode 100644 example/figure/1d/mesh/02a/testprj.py create mode 100644 example/figure/1d/mesh/03/testprj.py create mode 100644 example/figure/1d/mesh/03a/testprj.py create mode 100644 example/figure/1d/mesh/03b/testprj.py create mode 100644 example/figure/1d/mesh/03c/testprj.py create mode 100644 example/figure/1d/mesh/03d/testprj.py create mode 100644 example/figure/1d/mesh/03e/testprj.py create mode 100644 example/figure/1d/mesh/04/testprj.py create mode 100644 example/figure/1d/mesh/04a/testprj.py create mode 100644 example/figure/1d/mesh/04b/testprj.py create mode 100644 example/figure/1d/mesh/04c/testprj.py create mode 100644 example/figure/1d/mesh/04d/testprj.py create mode 100644 example/figure/1d/mesh/04e/testprj.py create mode 100644 example/figure/1d/mesh/04f/testprj.py create mode 100644 example/figure/1d/mesh/05/testprj.py create mode 100644 example/figure/1d/mesh/05a/testprj.py create mode 100644 example/figure/1d/mesh/05b/testprj.py create mode 100644 example/figure/1d/mesh/05c/testprj.py create mode 100644 example/figure/1d/mesh/05d/testprj.py create mode 100644 example/figure/1d/mesh/05e/testprj.py create mode 100644 example/figure/1d/mesh/05f/testprj.py create mode 100644 example/figure/1d/mesh/05g/testprj.py create mode 100644 example/figure/1d/periodic/01/testprj.py create mode 100644 example/figure/1d/periodic/01a/testprj.py create mode 100644 example/figure/1d/periodic/01b/testprj.py create mode 100644 example/figure/1d/periodic/01c/testprj.py create mode 100644 example/figure/1d/periodic/01d/testprj.py create mode 100644 example/figure/1d/periodic/01e/testprj.py create mode 100644 example/figure/1d/periodic/01f/testprj.py create mode 100644 example/figure/1d/periodic/01g/testprj.py create mode 100644 example/figure/1d/periodic/01g0/testprj.py create mode 100644 example/figure/1d/weno/LinearWeights/01/xi.py create mode 100644 example/figure/1d/weno/interplate/0st/01/testprj.py create mode 100644 example/figure/1d/weno/interplate/0st/01a/testprj.py create mode 100644 example/figure/1d/weno/interplate/0st/01b/testprj.py create mode 100644 example/figure/1d/weno/interplate/0st/01c/testprj.py create mode 100644 example/figure/1d/weno/interplate/0st/01d/testprj.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/01/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/01a/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/01b/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/01c/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/01d/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/01e/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/02/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/02a/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/02b/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/02c/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/02d/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/02e/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/02f/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/compute_integral/03/compute_integral.py create mode 100644 example/figure/1d/weno/interplate/counter/01/counter.py create mode 100644 example/figure/1d/weno/interplate/max_common_factor/01/max_common_factor.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/01/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/01a/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/01b/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/01c/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/01d/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/01e/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/02/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/02a/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/02b/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/02c/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/02d/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/02e/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/02f/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/03/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/03a/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/03b/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/03c/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/03d/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/polynomial_operations/03e/polynomial_operations.py create mode 100644 example/figure/1d/weno/interplate/smoothness_indicator/01/smoothness_indicator.py create mode 100644 example/figure/1d/weno/interplate/smoothness_indicator/01a/smoothness_indicator.py create mode 100644 example/figure/1d/weno/interplate/smoothness_indicator/01b/smoothness_indicator.py create mode 100644 example/figure/1d/weno/interplate/weno5_smoothness/01/weno5_smoothness_sympy.py create mode 100644 example/figure/1d/weno/interplate/xi/01/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/01a/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/02/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/02a/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/02b/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/02c/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/02d/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/02e/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/02f/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/02g/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/03/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/03a/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/03b/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/03c/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/03d/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/03e/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/04/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/04a/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/05/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/05a/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/05b/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/05c/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/05d/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/05e/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/05f/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/05g/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/06/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/06a/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/06b/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/06c/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/07/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/07a/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/07b/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/07c/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/07d/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/07e/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/07f/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/07g/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/07h/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/07i/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/07j/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/08/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/08a/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/08b/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/08c/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/08d/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/08e/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/08f/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/08g/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/09/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/09a/xi.py create mode 100644 example/figure/1d/weno/interplate/xi/09b/xi.py create mode 100644 example/figure/1d/weno/matrix/01/matrix.py create mode 100644 example/figure/1d/weno/matrix/01a/matrix.py create mode 100644 example/figure/1d/weno/matrix/01b/matrix.py create mode 100644 example/figure/1d/weno/matrix/01c/matrix.py create mode 100644 example/figure/1d/weno/matrix/02/matrix.py create mode 100644 example/figure/1d/weno/matrix/02a/matrix.py create mode 100644 example/figure/1d/weno/matrix/02b/matrix.py create mode 100644 example/figure/1d/weno/matrix/02c/matrix.py create mode 100644 example/figure/1d/weno/matrix/02d/matrix.py create mode 100644 example/figure/1d/weno/matrix/02e/matrix.py create mode 100644 example/figure/1d/weno/matrix/02f/matrix.py create mode 100644 example/figure/1d/weno/matrix/02g/matrix.py create mode 100644 example/figure/1d/weno/matrix/02h/matrix.py create mode 100644 example/figure/1d/weno/matrix/03/matrix.py create mode 100644 example/figure/1d/weno/matrix/03a/matrix.py create mode 100644 example/figure/1d/weno/smoothness/01/polynomial_operations.py create mode 100644 example/figure/1d/weno/some_help_code/01/test_sorting.py create mode 100644 example/figure/1d/weno/some_help_code/01a/weno_debug_sort.py create mode 100644 example/figure/1d/weno/some_help_code/01b/weno_debug_sort.py create mode 100644 example/figure/1d/weno/some_help_code/02/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/02a/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/02aa/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/02b/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/02c/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/02d/formulas.json create mode 100644 example/figure/1d/weno/some_help_code/02d/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/02e/formulas.json create mode 100644 example/figure/1d/weno/some_help_code/02e/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/03/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/03a/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/03b/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/03c/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/04/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/04a/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/04b/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/04c/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/04d/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/05/testprj.py create mode 100644 example/figure/1d/weno/some_help_code/05a/testprj.py create mode 100644 example/figure/1d/weno/wenoinfo/01/wenoinfo.py create mode 100644 example/weno-coef/crj/python/01e/crj.py create mode 100644 example/weno-coef/crj/python/01f/crj.py create mode 100644 example/weno-coef/crj/python/01g/crj.py create mode 100644 example/weno-coef/crj/python/expand_formula/01/testprj.py create mode 100644 example/weno-coef/crj/python/expand_formula/01a/testprj.py create mode 100644 example/weno-coef/crj/python/expand_formula/01b/testprj.py create mode 100644 example/weno-coef/crj/python/expand_formula/01b0/testprj.py create mode 100644 example/weno-coef/crj/python/expand_formula/01c/testprj.py create mode 100644 example/weno-coef/crj/python/expand_formula/01d/testprj.py create mode 100644 example/weno-coef/crj/python/expand_formula/01e/testprj.py create mode 100644 example/weno-coef/crj/python/expand_formula/01f/testprj.py create mode 100644 example/weno-coef/crj/python/expand_formula/01f0/testprj.py create mode 100644 example/weno-coef/crj/python/expand_formula/01g/testprj.py create mode 100644 example/weno-coef/crj/python/expand_formula/KKKKKK01f/testprj.py diff --git a/.gitignore b/.gitignore index cd101a39..29481f85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ **/build/* oneflow_bin/* **/project_tmp/* +**/__pycache__/* diff --git a/example/1d-linear-convection/weno3/python/02/weno3.py b/example/1d-linear-convection/weno3/python/02/weno3.py new file mode 100644 index 00000000..c3e7395e --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02/weno3.py @@ -0,0 +1,576 @@ +import numpy as np +import matplotlib.pyplot as plt + +# 初始条件 +def initial_condition(x): + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +# 理论解 +def analytical_solution(x, t, a, L): + # 初始条件沿 x - at 平移 + x_shifted = x - a * t + return initial_condition((x_shifted + L) % L) # 周期边界条件 + +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.up1_2m, cfd.up1_2p, cfd.flux, cfd) + for i in range(cfd.nx): + cfd.res[i] = -(cfd.flux[i + 1] - cfd.flux[i]) / cfd.mesh.dx + +def reconstruction(q, cfd): + if cfd.solver.interpolation == 0: + EnoReconstruction(q, cfd) + elif cfd.solver.interpolation == 1: + WenoReconstruction(q, cfd) + +def EnoReconstruction(q, cfd): + # Choose the stencil by ENO method + cfd.dd[0, 0:cfd.ntcell-1] = q[0:cfd.ntcell-1] + + for m in range(1, cfd.iorder): + for j in range(0, cfd.ntcell-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + for i in range(cfd.nx + 1): + cfd.il[i] = i - 1 + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + for i in range(cfd.nx + 1): + cfd.ir[i] = i + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruction u(j+1/2) + for i in range(cfd.nx + 1): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.up1_2m[i] = 0 + cfd.up1_2p[i] = 0 + for m in range(cfd.iorder): + cfd.up1_2m[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.up1_2p[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +#---------------------------------------------------------------------------# +#nonlinear weights for upwind direction +#---------------------------------------------------------------------------# +def wc3L(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +#---------------------------------------------------------------------------# +#nonlinear weights for downwind direction +#---------------------------------------------------------------------------# +def wc3R(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + +def weno3L_periodic(cfd,u,f): + #i:ist-1,ist,...,ied + #j:0,1,...,nx + for i in range(cfd.ist - 1, cfd.ied + 1): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +def weno3R_periodic(cfd,u,f): + #i:ist,ist+1,...,ied,ied+1 + #j:0,1,...,nx + for i in range(cfd.ist, cfd.ied + 2): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + + +def WenoReconstruction(q, cfd): + # Reconstruction u(j+1/2) + weno3L_periodic( cfd, q, cfd.up1_2m ) + weno3R_periodic( cfd, q, cfd.up1_2p ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +def inviscid_flux(up1_2m, up1_2p, flux, cfd): + if cfd.solver.iflux == 0: + rusanov_flux(up1_2m, up1_2p, flux, cfd) + else: + engquist_osher_flux(up1_2m, up1_2p, flux, cfd) + +def engquist_osher_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nx + 1): + u_L = up1_2m[i] + u_R = up1_2p[i] + + cp = 0.5 * ( cfd.solver.c + abs(cfd.solver.c) ) + cm = 0.5 * ( cfd.solver.c - abs(cfd.solver.c) ) + + flux[i] = cp * u_L + cm * u_R + +def rusanov_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nx + 1): + u_L = up1_2m[i] + u_R = up1_2p[i] + F_L = cfd.solver.c * u_L # 左状态通量 + F_R = cfd.solver.c * u_R # 右状态通量 + alpha = abs(cfd.solver.c) # 最大波速 + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + + +def boundary(u, cfd): + for i in range(-cfd.ighost, 1): + u[cfd.ist - 1 + i] = u[cfd.ied + i] + for i in range(1, cfd.ighost + 2): + u[cfd.ied + i] = u[cfd.ist - 1 + i] + +def update_oldfield(qn, q): + qn[:] = q[:] + +def init_coef( iorder, coef ): + if iorder == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif iorder == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif iorder == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif iorder == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif iorder == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif iorder == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif iorder == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +def init_field(cfd): + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta(cfd): + rk = cfd.solver.rk + if rk == 1: + runge_kutta_1(cfd) + elif rk == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +def runge_kutta_1(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_2(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_3(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def get_ordinal_numbers(order): + if order == 1: + return 'st' + elif order == 2: + return 'nd' + elif order == 3: + return 'rd' + else: + return 'th' + +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # 可视化 + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied+1]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + #计算理论解 + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.T, cfd.solver.c, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + ordinal1 = get_ordinal_numbers(cfd.iorder) + ordinal2 = get_ordinal_numbers(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.T:.3f} using {cfd.iorder}{ordinal1}-order WENO and {cfd.solver.rk}{ordinal2}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +def visualizeEno(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # 可视化 + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied+1]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + #计算理论解 + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.T, cfd.solver.c, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + ordinal1 = get_ordinal_numbers(cfd.iorder) + ordinal2 = get_ordinal_numbers(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.T:.3f} using {cfd.iorder}{ordinal1}-order ENO and {cfd.solver.rk}{ordinal2}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Solver: + def __init__(self): + #interpolation :0 Eno; 1 Weno + self.interpolation = 0 + self.iflux = 0 + self.rk = 1 + self.c = 1.0 + self.T = 0.625 + #self.dt = .0025 + self.dt = .025 + +class Cfd: + def __init__(self, solver, mesh, iorder): + self.solver = solver + self.mesh = mesh + self.nx = mesh.nx + self.iorder = iorder + self.ighost = iorder + self.ishift = self.ighost + 1 + self.ist = 0 + self.ishift + self.ied = self.nx - 1 + self.ishift + self.ntcell = self.nx + 2 * self.ishift + self.isize = iorder * (iorder + 1) + + self.il = np.zeros(self.nx + 1, dtype=int) + self.ir = np.zeros(self.nx + 1, dtype=int) + self.coef = np.zeros((iorder + 1, iorder)) + self.dd = np.zeros((iorder, self.ntcell)) + self.up1_2m = np.zeros(self.nx + 1) + self.up1_2p = np.zeros(self.nx + 1) + self.flux = np.zeros(self.nx + 1) + self.res = np.zeros(self.nx) + + # Field module variables + self.u = np.zeros(self.ntcell) + self.un = np.zeros(self.ntcell) + +def RunEno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},cfd{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + + return np.copy(cfd.u[cfd.ist:cfd.ied+1]) + +def RunWeno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},WENO{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + return np.copy(cfd.u[cfd.ist:cfd.ied+1]) + +def performEnoOrderAnalysis(): + iorder_max = 7 + mesh = Mesh() + solver = Solver() + #计算理论解 + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + u_list = [] + for iorder in range(1, iorder_max+1): + u = RunEno(solver, mesh, iorder) + u_list.append(u) + + plot_eno_OrderAnalysis(solver, mesh.xcc, u_list, u_analytical) + +def performEnoTimestepAnalysis(): + mesh = Mesh() + solver = Solver() + #计算理论解 + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + u_list = [] + dt_list = [] + solver.dt = 0.025/4 + #solver.dt = 0.025/16 + n = 12 + solver.rk = 3 + iorder = 7 + for i in range(0, n): + u = RunEno(solver, mesh, iorder) + u_list.append(u) + dt_list.append(solver.dt) + print(f'i={i+1},N={n},T={solver.T:.3f},dt={solver.dt},nt={int(solver.T/solver.dt)},runge_kutta{solver.rk},ENO{iorder},{fluxnames[solver.iflux]} FLUX') + solver.dt /= 2 + + plot_eno_TimestepAnalysis(solver, mesh.xcc, u_list, u_analytical, dt_list, iorder) + +def performEnoWenoAnalysis(): + mesh = Mesh() + solver = Solver() + #计算理论解 + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + solver.rk = 1 + solver.dt = 0.0025 + solver.iorder = 3 + + u_list = [] + solver.interpolation = 0 + u = RunEno(solver, mesh, solver.iorder) + u_list.append(u) + solver.interpolation = 1 + u = RunWeno(solver, mesh, solver.iorder) + u_list.append(u) + + plot_EnoWeno_Analysis(solver, mesh.xcc, u_list, u_analytical) + +def plot_eno_OrderAnalysis(solver, xcc, u_list, u_analytical): + # 定义一个包含不同颜色、线形和标记的列表 + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + ordinal = get_ordinal_numbers(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.T:.3f} using [1-7]th-order ENO and {solver.rk}{ordinal}-order Runge-Kutta methods') + for i in range(0, n): + lable = 'Numerical (Rusanov)ENO' + str(i+1) + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +def plot_EnoWeno_Analysis(solver, xcc, u_list, u_analytical): + # 定义一个包含不同颜色、线形和标记的列表 + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + ordinal = get_ordinal_numbers(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.T:.3f} using 3rd-order ENO&WENO and {solver.rk}{ordinal}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02a/weno3.py b/example/1d-linear-convection/weno3/python/02a/weno3.py new file mode 100644 index 00000000..352bb148 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02a/weno3.py @@ -0,0 +1,483 @@ +import numpy as np +import matplotlib.pyplot as plt + +# 初始条件 +def initial_condition(x): + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +# 理论解 +def analytical_solution(x, t, a, L): + # 初始条件沿 x - at 平移 + x_shifted = x - a * t + return initial_condition((x_shifted + L) % L) # 周期边界条件 + +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.up1_2m, cfd.up1_2p, cfd.flux, cfd) + for i in range(cfd.nx): + cfd.res[i] = -(cfd.flux[i + 1] - cfd.flux[i]) / cfd.mesh.dx + +def reconstruction(q, cfd): + if cfd.solver.interpolation == 0: + EnoReconstruction(q, cfd) + elif cfd.solver.interpolation == 1: + WenoReconstruction(q, cfd) + +def EnoReconstruction(q, cfd): + # Choose the stencil by ENO method + cfd.dd[0, 0:cfd.ntcell-1] = q[0:cfd.ntcell-1] + + for m in range(1, cfd.iorder): + for j in range(0, cfd.ntcell-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + for i in range(cfd.nx + 1): + cfd.il[i] = i - 1 + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + for i in range(cfd.nx + 1): + cfd.ir[i] = i + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruction u(j+1/2) + for i in range(cfd.nx + 1): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.up1_2m[i] = 0 + cfd.up1_2p[i] = 0 + for m in range(cfd.iorder): + cfd.up1_2m[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.up1_2p[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +#---------------------------------------------------------------------------# +#nonlinear weights for upwind direction +#---------------------------------------------------------------------------# +def wc3L(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +#---------------------------------------------------------------------------# +#nonlinear weights for downwind direction +#---------------------------------------------------------------------------# +def wc3R(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + +def weno3L_periodic(cfd,u,f): + #i:ist-1,ist,...,ied + #j:0,1,...,nx + for i in range(cfd.ist - 1, cfd.ied + 1): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +def weno3R_periodic(cfd,u,f): + #i:ist,ist+1,...,ied,ied+1 + #j:0,1,...,nx + for i in range(cfd.ist, cfd.ied + 2): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + + +def WenoReconstruction(q, cfd): + # Reconstruction u(j+1/2) + weno3L_periodic( cfd, q, cfd.up1_2m ) + weno3R_periodic( cfd, q, cfd.up1_2p ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +def inviscid_flux(up1_2m, up1_2p, flux, cfd): + if cfd.solver.iflux == 0: + rusanov_flux(up1_2m, up1_2p, flux, cfd) + else: + engquist_osher_flux(up1_2m, up1_2p, flux, cfd) + +def engquist_osher_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nx + 1): + u_L = up1_2m[i] + u_R = up1_2p[i] + + cp = 0.5 * ( cfd.solver.c + abs(cfd.solver.c) ) + cm = 0.5 * ( cfd.solver.c - abs(cfd.solver.c) ) + + flux[i] = cp * u_L + cm * u_R + +def rusanov_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nx + 1): + u_L = up1_2m[i] + u_R = up1_2p[i] + F_L = cfd.solver.c * u_L # 左状态通量 + F_R = cfd.solver.c * u_R # 右状态通量 + alpha = abs(cfd.solver.c) # 最大波速 + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + + +def boundary(u, cfd): + for i in range(-cfd.ighost, 1): + u[cfd.ist - 1 + i] = u[cfd.ied + i] + for i in range(1, cfd.ighost + 2): + u[cfd.ied + i] = u[cfd.ist - 1 + i] + +def update_oldfield(qn, q): + qn[:] = q[:] + +def init_coef( iorder, coef ): + if iorder == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif iorder == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif iorder == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif iorder == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif iorder == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif iorder == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif iorder == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +def init_field(cfd): + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta(cfd): + rk = cfd.solver.rk + if rk == 1: + runge_kutta_1(cfd) + elif rk == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +def runge_kutta_1(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_2(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_3(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def get_ordinal_numbers(order): + if order == 1: + return 'st' + elif order == 2: + return 'nd' + elif order == 3: + return 'rd' + else: + return 'th' + +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # 可视化 + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied+1]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + #计算理论解 + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.T, cfd.solver.c, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + ordinal1 = get_ordinal_numbers(cfd.iorder) + ordinal2 = get_ordinal_numbers(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.T:.3f} using {cfd.iorder}{ordinal1}-order WENO and {cfd.solver.rk}{ordinal2}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Solver: + def __init__(self): + #interpolation :0 Eno; 1 Weno + self.interpolation = 0 + self.iflux = 0 + self.rk = 1 + self.c = 1.0 + self.T = 0.625 + #self.dt = .0025 + self.dt = .025 + +class Cfd: + def __init__(self, solver, mesh, iorder): + self.solver = solver + self.mesh = mesh + self.nx = mesh.nx + self.iorder = iorder + self.ighost = iorder + self.ishift = self.ighost + 1 + self.ist = 0 + self.ishift + self.ied = self.nx - 1 + self.ishift + self.ntcell = self.nx + 2 * self.ishift + self.isize = iorder * (iorder + 1) + + self.il = np.zeros(self.nx + 1, dtype=int) + self.ir = np.zeros(self.nx + 1, dtype=int) + self.coef = np.zeros((iorder + 1, iorder)) + self.dd = np.zeros((iorder, self.ntcell)) + self.up1_2m = np.zeros(self.nx + 1) + self.up1_2p = np.zeros(self.nx + 1) + self.flux = np.zeros(self.nx + 1) + self.res = np.zeros(self.nx) + + # Field module variables + self.u = np.zeros(self.ntcell) + self.un = np.zeros(self.ntcell) + +def RunEno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},cfd{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + + return np.copy(cfd.u[cfd.ist:cfd.ied+1]) + +def RunWeno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},WENO{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + return np.copy(cfd.u[cfd.ist:cfd.ied+1]) + +def performEnoWenoAnalysis(): + mesh = Mesh() + solver = Solver() + #计算理论解 + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + solver.rk = 1 + solver.dt = 0.0025 + solver.iorder = 3 + + u_list = [] + solver.interpolation = 0 + u = RunEno(solver, mesh, solver.iorder) + u_list.append(u) + solver.interpolation = 1 + u = RunWeno(solver, mesh, solver.iorder) + u_list.append(u) + + plot_EnoWeno_Analysis(solver, mesh.xcc, u_list, u_analytical) + +def plot_EnoWeno_Analysis(solver, xcc, u_list, u_analytical): + # 定义一个包含不同颜色、线形和标记的列表 + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + ordinal = get_ordinal_numbers(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.T:.3f} using 3rd-order ENO&WENO and {solver.rk}{ordinal}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02b/weno3.py b/example/1d-linear-convection/weno3/python/02b/weno3.py new file mode 100644 index 00000000..60b76a2a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02b/weno3.py @@ -0,0 +1,489 @@ +import numpy as np +import matplotlib.pyplot as plt + +# 初始条件 +def initial_condition(x): + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +# 理论解 +def analytical_solution(x, t, a, L): + # 初始条件沿 x - at 平移 + x_shifted = x - a * t + return initial_condition((x_shifted + L) % L) # 周期边界条件 + +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.up1_2m, cfd.up1_2p, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i + 1] - cfd.flux[i]) / cfd.mesh.dx + +def reconstruction(q, cfd): + if cfd.solver.interpolation == 0: + EnoReconstruction(q, cfd) + elif cfd.solver.interpolation == 1: + WenoReconstruction(q, cfd) + +def EnoReconstruction(q, cfd): + # Choose the stencil by ENO method + cfd.dd[0, 0:cfd.ntcells-1] = q[0:cfd.ntcells-1] + + for m in range(1, cfd.iorder): + for j in range(0, cfd.ntcells-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + for i in range(cfd.nnodes): + cfd.il[i] = i - 1 + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + for i in range(cfd.nnodes): + cfd.ir[i] = i + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruction u(j+1/2) + for i in range(cfd.nnodes): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.up1_2m[i] = 0 + cfd.up1_2p[i] = 0 + for m in range(cfd.iorder): + cfd.up1_2m[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.up1_2p[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +#---------------------------------------------------------------------------# +#nonlinear weights for upwind direction +#---------------------------------------------------------------------------# +def wc3L(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +#---------------------------------------------------------------------------# +#nonlinear weights for downwind direction +#---------------------------------------------------------------------------# +def wc3R(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + +def weno3L_periodic(cfd,u,f): + #i:ist-1,ist,...,ied + #j:0,1,...,nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +def weno3R_periodic(cfd,u,f): + #i:ist,ist+1,...,ied,ied+1 + #j:0,1,...,nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + + +def WenoReconstruction(q, cfd): + # Reconstruction u(j+1/2) + weno3L_periodic( cfd, q, cfd.up1_2m ) + weno3R_periodic( cfd, q, cfd.up1_2p ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +def inviscid_flux(up1_2m, up1_2p, flux, cfd): + if cfd.solver.iflux == 0: + rusanov_flux(up1_2m, up1_2p, flux, cfd) + else: + engquist_osher_flux(up1_2m, up1_2p, flux, cfd) + +def engquist_osher_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nnodes): + u_L = up1_2m[i] + u_R = up1_2p[i] + + cp = 0.5 * ( cfd.solver.c + abs(cfd.solver.c) ) + cm = 0.5 * ( cfd.solver.c - abs(cfd.solver.c) ) + + flux[i] = cp * u_L + cm * u_R + +def rusanov_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nnodes): + u_L = up1_2m[i] + u_R = up1_2p[i] + F_L = cfd.solver.c * u_L # 左状态通量 + F_R = cfd.solver.c * u_R # 右状态通量 + alpha = abs(cfd.solver.c) # 最大波速 + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + + +def boundary(u, cfd): + for i in range(-cfd.ighost, 1): + u[cfd.ist - 1 + i] = u[cfd.ied -1 + i] + for i in range(1, cfd.ighost + 2): + u[cfd.ied - 1 + i] = u[cfd.ist - 1 + i] + +def update_oldfield(qn, q): + qn[:] = q[:] + +def init_coef( iorder, coef ): + if iorder == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif iorder == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif iorder == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif iorder == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif iorder == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif iorder == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif iorder == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta(cfd): + rk = cfd.solver.rk + if rk == 1: + runge_kutta_1(cfd) + elif rk == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +def runge_kutta_1(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_2(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_3(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def get_ordinal_numbers(order): + if order == 1: + return 'st' + elif order == 2: + return 'nd' + elif order == 3: + return 'rd' + else: + return 'th' + +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # 可视化 + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + #计算理论解 + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.T, cfd.solver.c, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + ordinal1 = get_ordinal_numbers(cfd.iorder) + ordinal2 = get_ordinal_numbers(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.T:.3f} using {cfd.iorder}{ordinal1}-order WENO and {cfd.solver.rk}{ordinal2}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Solver: + def __init__(self): + #interpolation :0 Eno; 1 Weno + self.interpolation = 0 + self.iflux = 0 + self.rk = 1 + self.c = 1.0 + self.T = 0.625 + #self.dt = .0025 + self.dt = .025 + +class Cfd: + def __init__(self, solver, mesh, iorder): + self.solver = solver + self.mesh = mesh + self.nx = mesh.nx + self.iorder = iorder + self.ighost = iorder + self.ishift = self.ighost + 1 + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ishift + self.ied = self.ncells + self.ishift + self.ntcells = self.ncells + 2 * self.ishift + print(f"self.ncells={self.ncells}") + print(f"self.iorder={self.iorder}") + print(f"self.ighost={self.ighost}") + print(f"self.ishift={self.ishift}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + self.il = np.zeros(self.nnodes, dtype=int) + self.ir = np.zeros(self.nnodes, dtype=int) + self.coef = np.zeros((iorder + 1, iorder)) + self.dd = np.zeros((iorder, self.ntcells)) + self.up1_2m = np.zeros(self.nnodes) + self.up1_2p = np.zeros(self.nnodes) + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) + + # Field module variables + self.u = np.zeros(self.ntcells) + self.un = np.zeros(self.ntcells) + +def RunEno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},cfd{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + + return np.copy(cfd.u[cfd.ist:cfd.ied]) + +def RunWeno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},WENO{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + return np.copy(cfd.u[cfd.ist:cfd.ied]) + +def performEnoWenoAnalysis(): + mesh = Mesh() + solver = Solver() + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + solver.rk = 1 + solver.dt = 0.0025 + solver.iorder = 3 + + u_list = [] + solver.interpolation = 0 + u = RunEno(solver, mesh, solver.iorder) + u_list.append(u) + solver.interpolation = 1 + u = RunWeno(solver, mesh, solver.iorder) + u_list.append(u) + + plot_EnoWeno_Analysis(solver, mesh.xcc, u_list, u_analytical) + +def plot_EnoWeno_Analysis(solver, xcc, u_list, u_analytical): + # 定义一个包含不同颜色、线形和标记的列表 + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + ordinal = get_ordinal_numbers(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.T:.3f} using 3rd-order ENO&WENO and {solver.rk}{ordinal}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02c/weno3.py b/example/1d-linear-convection/weno3/python/02c/weno3.py new file mode 100644 index 00000000..f73a9fad --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02c/weno3.py @@ -0,0 +1,490 @@ +import numpy as np +import matplotlib.pyplot as plt + +# 初始条件 +def initial_condition(x): + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +# 理论解 +def analytical_solution(x, t, a, L): + # 初始条件沿 x - at 平移 + x_shifted = x - a * t + return initial_condition((x_shifted + L) % L) # 周期边界条件 + +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.up1_2m, cfd.up1_2p, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i + 1] - cfd.flux[i]) / cfd.mesh.dx + +def reconstruction(q, cfd): + if cfd.solver.interpolation == 0: + EnoReconstruction(q, cfd) + elif cfd.solver.interpolation == 1: + WenoReconstruction(q, cfd) + +def EnoReconstruction(q, cfd): + # Choose the stencil by ENO method + cfd.dd[0, :] = q + + for m in range(1, cfd.iorder): + for j in range(0, cfd.ntcells-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + for i in range(cfd.nnodes): + cfd.il[i] = i - 1 + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + for i in range(cfd.nnodes): + cfd.ir[i] = i + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruction u(j+1/2) + for i in range(cfd.nnodes): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.up1_2m[i] = 0 + cfd.up1_2p[i] = 0 + for m in range(cfd.iorder): + cfd.up1_2m[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.up1_2p[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +#---------------------------------------------------------------------------# +#nonlinear weights for upwind direction +#---------------------------------------------------------------------------# +def wc3L(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +#---------------------------------------------------------------------------# +#nonlinear weights for downwind direction +#---------------------------------------------------------------------------# +def wc3R(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + +def weno3L_periodic(cfd,u,f): + #i:ist-1,ist,...,ied + #j:0,1,...,nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +def weno3R_periodic(cfd,u,f): + #i:ist,ist+1,...,ied,ied+1 + #j:0,1,...,nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + + +def WenoReconstruction(q, cfd): + # Reconstruction u(j+1/2) + weno3L_periodic( cfd, q, cfd.up1_2m ) + weno3R_periodic( cfd, q, cfd.up1_2p ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +def inviscid_flux(up1_2m, up1_2p, flux, cfd): + if cfd.solver.iflux == 0: + rusanov_flux(up1_2m, up1_2p, flux, cfd) + else: + engquist_osher_flux(up1_2m, up1_2p, flux, cfd) + +def engquist_osher_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nnodes): + u_L = up1_2m[i] + u_R = up1_2p[i] + + cp = 0.5 * ( cfd.solver.c + abs(cfd.solver.c) ) + cm = 0.5 * ( cfd.solver.c - abs(cfd.solver.c) ) + + flux[i] = cp * u_L + cm * u_R + +def rusanov_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nnodes): + u_L = up1_2m[i] + u_R = up1_2p[i] + F_L = cfd.solver.c * u_L # 左状态通量 + F_R = cfd.solver.c * u_R # 右状态通量 + alpha = abs(cfd.solver.c) # 最大波速 + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + + +def boundary(u, cfd): + for i in range(-cfd.ighost, 1): + u[cfd.ist - 1 + i] = u[cfd.ied -1 + i] + for i in range(1, cfd.ighost + 2): + u[cfd.ied - 1 + i] = u[cfd.ist - 1 + i] + +def update_oldfield(qn, q): + qn[:] = q[:] + +def init_coef( iorder, coef ): + if iorder == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif iorder == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif iorder == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif iorder == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif iorder == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif iorder == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif iorder == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta(cfd): + rk = cfd.solver.rk + if rk == 1: + runge_kutta_1(cfd) + elif rk == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +def runge_kutta_1(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_2(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_3(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def get_ordinal_numbers(order): + if order == 1: + return 'st' + elif order == 2: + return 'nd' + elif order == 3: + return 'rd' + else: + return 'th' + +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # 可视化 + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + #计算理论解 + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.T, cfd.solver.c, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + ordinal1 = get_ordinal_numbers(cfd.iorder) + ordinal2 = get_ordinal_numbers(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.T:.3f} using {cfd.iorder}{ordinal1}-order WENO and {cfd.solver.rk}{ordinal2}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Solver: + def __init__(self): + #interpolation :0 Eno; 1 Weno + self.interpolation = 0 + self.iflux = 0 + self.rk = 1 + self.c = 1.0 + self.T = 0.625 + #self.dt = .0025 + self.dt = .025 + +class Cfd: + def __init__(self, solver, mesh, iorder): + self.solver = solver + self.mesh = mesh + self.nx = mesh.nx + self.iorder = iorder + self.ighost = iorder + self.ishift = self.ighost + 1 + #self.ishift = self.ighost + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ishift + self.ied = self.ncells + self.ishift + self.ntcells = self.ncells + 2 * self.ishift + print(f"self.ncells={self.ncells}") + print(f"self.iorder={self.iorder}") + print(f"self.ighost={self.ighost}") + print(f"self.ishift={self.ishift}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + self.il = np.zeros(self.nnodes, dtype=int) + self.ir = np.zeros(self.nnodes, dtype=int) + self.coef = np.zeros((iorder + 1, iorder)) + self.dd = np.zeros((iorder, self.ntcells)) + self.up1_2m = np.zeros(self.nnodes) + self.up1_2p = np.zeros(self.nnodes) + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) + + # Field module variables + self.u = np.zeros(self.ntcells) + self.un = np.zeros(self.ntcells) + +def RunEno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},cfd{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + + return np.copy(cfd.u[cfd.ist:cfd.ied]) + +def RunWeno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},WENO{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + return np.copy(cfd.u[cfd.ist:cfd.ied]) + +def performEnoWenoAnalysis(): + mesh = Mesh() + solver = Solver() + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + solver.rk = 1 + solver.dt = 0.0025 + solver.iorder = 3 + + u_list = [] + solver.interpolation = 0 + u = RunEno(solver, mesh, solver.iorder) + u_list.append(u) + solver.interpolation = 1 + u = RunWeno(solver, mesh, solver.iorder) + u_list.append(u) + + plot_EnoWeno_Analysis(solver, mesh.xcc, u_list, u_analytical) + +def plot_EnoWeno_Analysis(solver, xcc, u_list, u_analytical): + # 定义一个包含不同颜色、线形和标记的列表 + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + ordinal = get_ordinal_numbers(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.T:.3f} using 3rd-order ENO&WENO and {solver.rk}{ordinal}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02d/weno3.py b/example/1d-linear-convection/weno3/python/02d/weno3.py new file mode 100644 index 00000000..437652c9 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02d/weno3.py @@ -0,0 +1,526 @@ +import numpy as np +import matplotlib.pyplot as plt + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.up1_2m, cfd.up1_2p, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + if cfd.solver.interpolation == 0: + EnoReconstruction(q, cfd) + elif cfd.solver.interpolation == 1: + WenoReconstruction(q, cfd) + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstruction(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.iorder): + for j in range(0, cfd.ntcells-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.nnodes): + cfd.il[i] = i - 1 + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + # Select right-biased stencil for each node + for i in range(cfd.nnodes): + cfd.ir[i] = i + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.nnodes): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.up1_2m[i] = 0 + cfd.up1_2p[i] = 0 + for m in range(cfd.iorder): + cfd.up1_2m[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.up1_2p[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# WENO (Weighted Essentially Non-Oscillatory) reconstruction +def WenoReconstruction(q, cfd): + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.up1_2m ) + weno3R_periodic( cfd, q, cfd.up1_2p ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(up1_2m, up1_2p, flux, cfd): + if cfd.solver.iflux == 0: + rusanov_flux(up1_2m, up1_2p, flux, cfd) + else: + engquist_osher_flux(up1_2m, up1_2p, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(up1_2m, up1_2p, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = up1_2m[i] + u_R = up1_2p[i] + F_L = cfd.solver.c * u_L # Flux from left state + F_R = cfd.solver.c * u_R # Flux from right state + alpha = abs(cfd.solver.c) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + +def engquist_osher_flux(up1_2m, up1_2p, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.solver.c + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + + u_L = up1_2m[i] + u_R = up1_2p[i] + + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.ighost): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.ighost): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( iorder, coef ): + if iorder == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif iorder == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif iorder == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif iorder == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif iorder == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif iorder == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif iorder == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk = cfd.solver.rk + if rk == 1: + runge_kutta_1(cfd) + elif rk == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Get ordinal suffix for number formatting (st, nd, rd, th) +def get_ordinal_numbers(order): + if order == 1: + return 'st' + elif order == 2: + return 'nd' + elif order == 3: + return 'rd' + else: + return 'th' + +# Visualize numerical and analytical solutions +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # Visualization setup + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + # Compute analytical solution + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.T, cfd.solver.c, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + ordinal1 = get_ordinal_numbers(cfd.iorder) + ordinal2 = get_ordinal_numbers(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.T:.3f} using {cfd.iorder}{ordinal1}-order WENO and {cfd.solver.rk}{ordinal2}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +# Solver class: stores numerical method parameters +class Solver: + def __init__(self): + # interpolation: 0 for ENO, 1 for WENO + self.interpolation = 0 + self.iflux = 0 + self.rk = 1 + self.c = 1.0 + self.T = 0.625 + #self.dt = .0025 + self.dt = .025 + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, solver, mesh, iorder): + self.solver = solver + self.mesh = mesh + self.nx = mesh.nx + self.iorder = iorder + self.ighost = iorder # Number of ghost cells + self.ishift = self.ighost + 1 + #self.ishift = self.ighost + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ishift # Start index of physical cells + self.ied = self.ncells + self.ist # End index of physical cells + self.ntcells = self.ncells + 2 * self.ishift # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.iorder={self.iorder}") + print(f"self.ighost={self.ighost}") + print(f"self.ishift={self.ishift}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + # Stencil selection arrays + self.il = np.zeros(self.nnodes, dtype=int) + self.ir = np.zeros(self.nnodes, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((iorder + 1, iorder)) + self.dd = np.zeros((iorder, self.ntcells)) + + # Interface values and fluxes + self.up1_2m = np.zeros(self.nnodes) # Left interface value + self.up1_2p = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + init_coef(self.iorder, self.coef) + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.solver.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.solver.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.solver.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + solver = Solver() + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + solver.rk = 1 + solver.dt = 0.0025 + solver.iorder = 3 + + u_list = [] + # ENO + solver.interpolation = 0 + cfd = Cfd(solver, mesh, iorder=3) + init_field(cfd) + u_eno = run_simulation(cfd, solver.T) + u_list.append(u_eno) + + # WENO + solver.interpolation = 1 + cfd = Cfd(solver, mesh, iorder=3) + init_field(cfd) + u_weno = run_simulation(cfd, solver.T) + u_list.append(u_weno) + + + plot_EnoWeno_Analysis(solver, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(solver, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + ordinal = get_ordinal_numbers(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.T:.3f} using 3rd-order ENO&WENO and {solver.rk}{ordinal}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02e/weno3.py b/example/1d-linear-convection/weno3/python/02e/weno3.py new file mode 100644 index 00000000..6184625e --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02e/weno3.py @@ -0,0 +1,515 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.up1_2m, cfd.up1_2p, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + if cfd.solver.interpolation == 0: + EnoReconstruction(q, cfd) + elif cfd.solver.interpolation == 1: + WenoReconstruction(q, cfd) + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstruction(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.iorder): + for j in range(0, cfd.ntcells-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.nnodes): + cfd.il[i] = i - 1 + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + # Select right-biased stencil for each node + for i in range(cfd.nnodes): + cfd.ir[i] = i + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.nnodes): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.up1_2m[i] = 0 + cfd.up1_2p[i] = 0 + for m in range(cfd.iorder): + cfd.up1_2m[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.up1_2p[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# WENO (Weighted Essentially Non-Oscillatory) reconstruction +def WenoReconstruction(q, cfd): + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.up1_2m ) + weno3R_periodic( cfd, q, cfd.up1_2p ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(up1_2m, up1_2p, flux, cfd): + if cfd.solver.flux_type == 0: + rusanov_flux(up1_2m, up1_2p, flux, cfd) + else: + engquist_osher_flux(up1_2m, up1_2p, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(up1_2m, up1_2p, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = up1_2m[i] + u_R = up1_2p[i] + F_L = cfd.solver.wave_speed * u_L # Flux from left state + F_R = cfd.solver.wave_speed * u_R # Flux from right state + alpha = abs(cfd.solver.wave_speed) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + +def engquist_osher_flux(up1_2m, up1_2p, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.solver.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = up1_2m[i] + u_R = up1_2p[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.ighost): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.ighost): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( iorder, coef ): + if iorder == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif iorder == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif iorder == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif iorder == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif iorder == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif iorder == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif iorder == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk = cfd.solver.rk + if rk == 1: + runge_kutta_1(cfd) + elif rk == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Visualize numerical and analytical solutions +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # Visualization setup + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + # Compute analytical solution + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.final_time, cfd.solver.wave_speed, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + + p = inflect.engine() + iorder_str = p.ordinal(cfd.iorder) + rk_str = p.ordinal(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.final_time:.3f} using {iorder_str}-order WENO and {rk_str}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class SimulationConfig: + def __init__(self): + self.interpolation = 0 # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, solver, mesh, iorder): + self.solver = solver + self.mesh = mesh + self.nx = mesh.nx + self.iorder = iorder + self.ighost = iorder # Number of ghost cells + self.ishift = self.ighost + 1 + #self.ishift = self.ighost + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ishift # Start index of physical cells + self.ied = self.ncells + self.ist # End index of physical cells + self.ntcells = self.ncells + 2 * self.ishift # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.iorder={self.iorder}") + print(f"self.ighost={self.ighost}") + print(f"self.ishift={self.ishift}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + # Stencil selection arrays + self.il = np.zeros(self.nnodes, dtype=int) + self.ir = np.zeros(self.nnodes, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((iorder + 1, iorder)) + self.dd = np.zeros((iorder, self.ntcells)) + + # Interface values and fluxes + self.up1_2m = np.zeros(self.nnodes) # Left interface value + self.up1_2p = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + init_coef(self.iorder, self.coef) + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.solver.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.solver.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.solver.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + solver = SimulationConfig() + u_analytical = analytical_solution(mesh.xcc, solver.final_time, solver.wave_speed, mesh.L) + + solver.rk = 1 + solver.dt = 0.0025 + solver.iorder = 3 + + u_list = [] + # ENO + solver.interpolation = 0 + cfd = Cfd(solver, mesh, iorder=3) + init_field(cfd) + u_eno = run_simulation(cfd, solver.final_time) + u_list.append(u_eno) + + # WENO + solver.interpolation = 1 + cfd = Cfd(solver, mesh, iorder=3) + init_field(cfd) + u_weno = run_simulation(cfd, solver.final_time) + u_list.append(u_weno) + + plot_EnoWeno_Analysis(solver, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(solver, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02f/weno3.py b/example/1d-linear-convection/weno3/python/02f/weno3.py new file mode 100644 index 00000000..5a1d33ee --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02f/weno3.py @@ -0,0 +1,549 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.q_face_left, cfd.q_face_right, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + if cfd.config.reconstruction_scheme == 0: + EnoReconstruction(q, cfd) + elif cfd.config.reconstruction_scheme == 1: + WenoReconstruction(q, cfd) + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstruction(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.spatial_order): + for j in range(0, cfd.ntcells-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.nnodes): + cfd.il[i] = i - 1 + for m in range(1, cfd.spatial_order): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + # Select right-biased stencil for each node + for i in range(cfd.nnodes): + cfd.ir[i] = i + for m in range(1, cfd.spatial_order): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.nnodes): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.q_face_left[i] = 0 + cfd.q_face_right[i] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.q_face_right[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstructionOld(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.spatial_order): + for j in range(cfd.ntcells-m): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.ist-1,cfd.ied+1): + cfd.lmc[i] = i + for m in range(1, cfd.spatial_order): + if abs(cfd.dd[m, cfd.lmc[i]-1]) < abs(cfd.dd[m, cfd.lmc[i]]): + cfd.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.ist,cfd.ied+1): + j = i - cfd.ist + k1 = cfd.lmc[i-1] + k2 = cfd.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + cfd.q_face_left[j] = 0 + cfd.q_face_right[j] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[j] += q[k1 + m] * cfd.coef[r1+1, m] + cfd.q_face_right[j] += q[k2 + m] * cfd.coef[r2, m] + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# WENO (Weighted Essentially Non-Oscillatory) reconstruction +def WenoReconstruction(q, cfd): + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.q_face_left ) + weno3R_periodic( cfd, q, cfd.q_face_right ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + F_L = cfd.config.wave_speed * u_L # Flux from left state + F_R = cfd.config.wave_speed * u_R # Flux from right state + alpha = abs(cfd.config.wave_speed) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.ighost): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.ighost): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Visualize numerical and analytical solutions +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # Visualization setup + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + # Compute analytical solution + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.config.final_time, cfd.config.wave_speed, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + + p = inflect.engine() + iorder_str = p.ordinal(cfd.spatial_order) + rk_str = p.ordinal(cfd.config.rk_order) + plt.title(f'1D Convection Equation at t = {cfd.config.final_time:.3f} using {iorder_str}-order WENO and {rk_str}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class SimulationConfig: + def __init__(self): + self.reconstruction_scheme = 0 # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh, spatial_order): + self.config = config + self.mesh = mesh + self.nx = mesh.nx + self.spatial_order = spatial_order + self.ighost = spatial_order # Number of ghost cells + self.ishift = self.ighost + 1 + #self.ishift = self.ighost + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ishift # Start index of physical cells + self.ied = self.ncells + self.ist # End index of physical cells + self.ntcells = self.ncells + 2 * self.ishift # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.spatial_order={self.spatial_order}") + print(f"self.ighost={self.ighost}") + print(f"self.ishift={self.ishift}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + # Stencil selection arrays + self.il = np.zeros(self.nnodes, dtype=int) + self.ir = np.zeros(self.nnodes, dtype=int) + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + # Interface values and fluxes + self.q_face_left = np.zeros(self.nnodes) # Left interface value + self.q_face_right = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + init_coef(self.spatial_order, self.coef) + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + config = SimulationConfig() + u_analytical = analytical_solution(mesh.xcc, config.final_time, config.wave_speed, mesh.L) + + config.rk_order = 1 + config.dt = 0.0025 + + u_list = [] + # ENO + config.reconstruction_scheme = 0 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_eno = run_simulation(cfd, config.final_time) + u_list.append(u_eno) + + # WENO + config.reconstruction_scheme = 1 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_weno = run_simulation(cfd, config.final_time) + u_list.append(u_weno) + + plot_EnoWeno_Analysis(config, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02g/weno3.py b/example/1d-linear-convection/weno3/python/02g/weno3.py new file mode 100644 index 00000000..e0d87613 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02g/weno3.py @@ -0,0 +1,513 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.q_face_left, cfd.q_face_right, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + if cfd.config.reconstruction_scheme == 0: + EnoReconstruction(q, cfd) + elif cfd.config.reconstruction_scheme == 1: + WenoReconstruction(q, cfd) + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstruction(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.spatial_order): + for j in range(cfd.ntcells-m): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.ist-1,cfd.ied+1): + cfd.lmc[i] = i + for m in range(1, cfd.spatial_order): + if abs(cfd.dd[m, cfd.lmc[i]-1]) < abs(cfd.dd[m, cfd.lmc[i]]): + cfd.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.ist,cfd.ied+1): + j = i - cfd.ist + k1 = cfd.lmc[i-1] + k2 = cfd.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + cfd.q_face_left[j] = 0 + cfd.q_face_right[j] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[j] += q[k1 + m] * cfd.coef[r1+1, m] + cfd.q_face_right[j] += q[k2 + m] * cfd.coef[r2, m] + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# WENO (Weighted Essentially Non-Oscillatory) reconstruction +def WenoReconstruction(q, cfd): + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.q_face_left ) + weno3R_periodic( cfd, q, cfd.q_face_right ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + F_L = cfd.config.wave_speed * u_L # Flux from left state + F_R = cfd.config.wave_speed * u_R # Flux from right state + alpha = abs(cfd.config.wave_speed) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.ighost): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.ighost): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.5 * cfd.un[i] + 0.5 * cfd.u[i] + 0.5 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + #for i in range(cfd.ist, cfd.ied): + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.75 * cfd.un[i] + 0.25 * cfd.u[i] + 0.25 * dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = c1 * cfd.un[i] + c2 * cfd.u[i] + c3 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Visualize numerical and analytical solutions +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # Visualization setup + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + # Compute analytical solution + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.config.final_time, cfd.config.wave_speed, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + + p = inflect.engine() + iorder_str = p.ordinal(cfd.spatial_order) + rk_str = p.ordinal(cfd.config.rk_order) + plt.title(f'1D Convection Equation at t = {cfd.config.final_time:.3f} using {iorder_str}-order WENO and {rk_str}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class SimulationConfig: + def __init__(self): + self.reconstruction_scheme = 0 # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh, spatial_order): + self.config = config + self.mesh = mesh + self.nx = mesh.nx + self.spatial_order = spatial_order + self.ighost = spatial_order # Number of ghost cells + self.ishift = self.ighost + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ishift # Start index of physical cells + self.ied = self.ncells + self.ist # End index of physical cells + self.ntcells = self.ncells + 2 * self.ishift # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.spatial_order={self.spatial_order}") + print(f"self.ighost={self.ighost}") + print(f"self.ishift={self.ishift}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + # Stencil selection arrays + self.il = np.zeros(self.nnodes, dtype=int) + self.ir = np.zeros(self.nnodes, dtype=int) + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + # Interface values and fluxes + self.q_face_left = np.zeros(self.nnodes) # Left interface value + self.q_face_right = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + init_coef(self.spatial_order, self.coef) + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + config = SimulationConfig() + u_analytical = analytical_solution(mesh.xcc, config.final_time, config.wave_speed, mesh.L) + + #config.rk_order = 1 + config.rk_order = 3 + config.dt = 0.0025 + + u_list = [] + # ENO + config.reconstruction_scheme = 0 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_eno = run_simulation(cfd, config.final_time) + u_list.append(u_eno) + + # WENO + config.reconstruction_scheme = 1 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_weno = run_simulation(cfd, config.final_time) + u_list.append(u_weno) + + plot_EnoWeno_Analysis(config, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02h/weno3.py b/example/1d-linear-convection/weno3/python/02h/weno3.py new file mode 100644 index 00000000..2aea59b1 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02h/weno3.py @@ -0,0 +1,552 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.q_face_left, cfd.q_face_right, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + if cfd.config.reconstruction_scheme == 0: + #EnoReconstruction(q, cfd) + cfd.reconstructor.reconstruct(q, cfd) + elif cfd.config.reconstruction_scheme == 1: + #WenoReconstruction(q, cfd) + cfd.reconstructor.reconstruct(q, cfd) + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstruction(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.spatial_order): + for j in range(cfd.ntcells-m): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.ist-1,cfd.ied+1): + cfd.lmc[i] = i + for m in range(1, cfd.spatial_order): + if abs(cfd.dd[m, cfd.lmc[i]-1]) < abs(cfd.dd[m, cfd.lmc[i]]): + cfd.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.ist,cfd.ied+1): + j = i - cfd.ist + k1 = cfd.lmc[i-1] + k2 = cfd.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + cfd.q_face_left[j] = 0 + cfd.q_face_right[j] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[j] += q[k1 + m] * cfd.coef[r1+1, m] + cfd.q_face_right[j] += q[k2 + m] * cfd.coef[r2, m] + +# WENO (Weighted Essentially Non-Oscillatory) reconstruction +def WenoReconstruction(q, cfd): + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.q_face_left ) + weno3R_periodic( cfd, q, cfd.q_face_right ) + + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + F_L = cfd.config.wave_speed * u_L # Flux from left state + F_R = cfd.config.wave_speed * u_R # Flux from right state + alpha = abs(cfd.config.wave_speed) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.ighost): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.ighost): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.5 * cfd.un[i] + 0.5 * cfd.u[i] + 0.5 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + #for i in range(cfd.ist, cfd.ied): + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.75 * cfd.un[i] + 0.25 * cfd.u[i] + 0.25 * dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = c1 * cfd.un[i] + c2 * cfd.u[i] + c3 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Visualize numerical and analytical solutions +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # Visualization setup + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + # Compute analytical solution + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.config.final_time, cfd.config.wave_speed, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + + p = inflect.engine() + iorder_str = p.ordinal(cfd.spatial_order) + rk_str = p.ordinal(cfd.config.rk_order) + plt.title(f'1D Convection Equation at t = {cfd.config.final_time:.3f} using {iorder_str}-order WENO and {rk_str}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class SimulationConfig: + def __init__(self): + self.reconstruction_scheme = 0 # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + print(f"Reconstructor:reconstruct") + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + def reconstruct(self, q, cfd): + #print(f"EnoReconstructor:reconstruct") + EnoReconstruction(q, cfd) + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + #print(f"WenoReconstructor:reconstruct") + WenoReconstruction(q, cfd) + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh, spatial_order): + self.config = config + self.mesh = mesh + self.nx = mesh.nx + self.spatial_order = spatial_order + self.ighost = spatial_order # Number of ghost cells + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ighost # Start index of physical cells + self.ied = self.ist + self.ncells # End index of physical cells + self.ntcells = self.ncells + 2 * self.ighost # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.spatial_order={self.spatial_order}") + print(f"self.ighost={self.ighost}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + self.createReconstructor() + + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + # Interface values and fluxes + self.q_face_left = np.zeros(self.nnodes) # Left interface value + self.q_face_right = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + init_coef(self.spatial_order, self.coef) + + def createReconstructor(self): + if self.config.reconstruction_scheme == 0: #ENO + self.reconstructor = EnoReconstructor(self.spatial_order, self.ntcells) + elif self.config.reconstruction_scheme == 1: #WENO + self.reconstructor = WenoReconstructor() + + #self.reconstructor.reconstruct() + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + config = SimulationConfig() + u_analytical = analytical_solution(mesh.xcc, config.final_time, config.wave_speed, mesh.L) + + #config.rk_order = 1 + config.rk_order = 3 + config.dt = 0.0025 + + u_list = [] + # ENO + config.reconstruction_scheme = 0 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_eno = run_simulation(cfd, config.final_time) + u_list.append(u_eno) + + # WENO + config.reconstruction_scheme = 1 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_weno = run_simulation(cfd, config.final_time) + u_list.append(u_weno) + + plot_EnoWeno_Analysis(config, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02i/weno3.py b/example/1d-linear-convection/weno3/python/02i/weno3.py new file mode 100644 index 00000000..52dc13ed --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02i/weno3.py @@ -0,0 +1,584 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.q_face_left, cfd.q_face_right, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstruction(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.spatial_order): + for j in range(cfd.ntcells-m): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.ist-1,cfd.ied+1): + cfd.lmc[i] = i + for m in range(1, cfd.spatial_order): + if abs(cfd.dd[m, cfd.lmc[i]-1]) < abs(cfd.dd[m, cfd.lmc[i]]): + cfd.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.ist,cfd.ied+1): + j = i - cfd.ist + k1 = cfd.lmc[i-1] + k2 = cfd.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + cfd.q_face_left[j] = 0 + cfd.q_face_right[j] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[j] += q[k1 + m] * cfd.coef[r1+1, m] + cfd.q_face_right[j] += q[k2 + m] * cfd.coef[r2, m] + +# WENO (Weighted Essentially Non-Oscillatory) reconstruction +def WenoReconstruction(q, cfd): + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.q_face_left ) + weno3R_periodic( cfd, q, cfd.q_face_right ) + + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + F_L = cfd.config.wave_speed * u_L # Flux from left state + F_R = cfd.config.wave_speed * u_R # Flux from right state + alpha = abs(cfd.config.wave_speed) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.ighost): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.ighost): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.5 * cfd.un[i] + 0.5 * cfd.u[i] + 0.5 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + #for i in range(cfd.ist, cfd.ied): + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.75 * cfd.un[i] + 0.25 * cfd.u[i] + 0.25 * dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = c1 * cfd.un[i] + c2 * cfd.u[i] + c3 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Visualize numerical and analytical solutions +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # Visualization setup + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + # Compute analytical solution + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.config.final_time, cfd.config.wave_speed, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + + p = inflect.engine() + iorder_str = p.ordinal(cfd.spatial_order) + rk_str = p.ordinal(cfd.config.rk_order) + plt.title(f'1D Convection Equation at t = {cfd.config.final_time:.3f} using {iorder_str}-order WENO and {rk_str}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class SimulationConfig: + def __init__(self): + self.reconstruction_scheme = 0 # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + print(f"Reconstructor:reconstruct") + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + #EnoReconstruction(q, cfd) + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.ist-1,cfd.ied+1): + self.lmc[i] = i + for m in range(1, cfd.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.ist,cfd.ied+1): + j = i - cfd.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + cfd.q_face_left[j] = 0 + cfd.q_face_right[j] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + cfd.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + #print(f"WenoReconstructor:reconstruct") + #WenoReconstruction(q, cfd) + + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.q_face_left ) + weno3R_periodic( cfd, q, cfd.q_face_right ) + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh, spatial_order): + self.config = config + self.mesh = mesh + self.nx = mesh.nx + self.spatial_order = spatial_order + self.ighost = spatial_order # Number of ghost cells + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ighost # Start index of physical cells + self.ied = self.ist + self.ncells # End index of physical cells + self.ntcells = self.ncells + 2 * self.ighost # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.spatial_order={self.spatial_order}") + print(f"self.ighost={self.ighost}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + self.createReconstructor() + + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + # Interface values and fluxes + self.q_face_left = np.zeros(self.nnodes) # Left interface value + self.q_face_right = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + init_coef(self.spatial_order, self.coef) + + def createReconstructor(self): + if self.config.reconstruction_scheme == 0: #ENO + self.reconstructor = EnoReconstructor(self.spatial_order, self.ntcells) + elif self.config.reconstruction_scheme == 1: #WENO + self.reconstructor = WenoReconstructor() + + #self.reconstructor.reconstruct() + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + config = SimulationConfig() + u_analytical = analytical_solution(mesh.xcc, config.final_time, config.wave_speed, mesh.L) + + #config.rk_order = 1 + config.rk_order = 3 + config.dt = 0.0025 + + u_list = [] + # ENO + config.reconstruction_scheme = 0 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_eno = run_simulation(cfd, config.final_time) + u_list.append(u_eno) + + # WENO + config.reconstruction_scheme = 1 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_weno = run_simulation(cfd, config.final_time) + u_list.append(u_weno) + + plot_EnoWeno_Analysis(config, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02j/weno3.py b/example/1d-linear-convection/weno3/python/02j/weno3.py new file mode 100644 index 00000000..ff51dd1c --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02j/weno3.py @@ -0,0 +1,512 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.q_face_left, cfd.q_face_right, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.nghosts): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.nghosts): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.5 * cfd.un[i] + 0.5 * cfd.u[i] + 0.5 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.75 * cfd.un[i] + 0.25 * cfd.u[i] + 0.25 * dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = c1 * cfd.un[i] + c2 * cfd.u[i] + c3 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.ist-1,cfd.ied+1): + self.lmc[i] = i + for m in range(1, cfd.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.ist,cfd.ied+1): + j = i - cfd.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + cfd.q_face_left[j] = 0 + cfd.q_face_right[j] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + cfd.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.q_face_left ) + weno3R_periodic( cfd, q, cfd.q_face_right ) + +class SimulationConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_mesh(self, mesh): + """设置网格""" + self.mesh = mesh + return self + + def with_reconstruction(self, scheme, order=None): + """设置重建方案""" + self.recon_scheme = scheme + if order is not None: + self.spatial_order = order + + if scheme == "weno" and order is None: + self.spatial_order = 5 # WENO默认5阶 + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh, spatial_order): + self.config = config + self.mesh = mesh + self.nx = mesh.nx + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + + self.spatial_order = spatial_order + self.nghosts = spatial_order # Number of ghost cells + self.ist = 0 + self.nghosts # Start index of physical cells + self.ied = self.ist + self.ncells # End index of physical cells + self.ntcells = self.ncells + 2 * self.nghosts # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.spatial_order={self.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + self.createReconstructor() + + # Interface values and fluxes + self.q_face_left = np.zeros(self.nnodes) # Left interface value + self.q_face_right = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + def createReconstructor(self): + if self.config.recon_scheme == "eno": #ENO + self.reconstructor = EnoReconstructor(self.spatial_order, self.ntcells) + elif self.config.recon_scheme == "weno": #WENO + self.reconstructor = WenoReconstructor() + + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + config = SimulationConfig() + + #config.rk_order = 1 + config.rk_order = 3 + config.dt = 0.0025 + + u_list = [] + # ENO + config.with_reconstruction("eno",3) + + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_eno = run_simulation(cfd, config.final_time) + u_list.append(u_eno) + + # WENO + config.with_reconstruction("weno",3) + + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_weno = run_simulation(cfd, config.final_time) + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config.final_time, config.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03/weno3.py b/example/1d-linear-convection/weno3/python/03/weno3.py new file mode 100644 index 00000000..85f722b8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03/weno3.py @@ -0,0 +1,610 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + weno3L_periodic( cfd, q, solution.q_face_left ) + weno3R_periodic( cfd, q, solution.q_face_right ) + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, mesh, config): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.mesh = mesh + self.config = config + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, domain): + self.config = config + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = self._create_reconstructor() + + def _create_reconstructor(self): + if self.config.recon_scheme == "eno": #ENO + reconstructor = EnoReconstructor(self.config.spatial_order, self.domain.ntcells) + elif self.config.recon_scheme == "weno": #WENO + reconstructor = WenoReconstructor() + return reconstructor + + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + + domain = cfd.domain + solution = cfd.solution + + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + domain_eno3 = ComputationalDomain(mesh, config_eno3) + cfd_eno3 = Cfd(config_eno3, domain_eno3) + + + u_list = [] + # ENO + init_field(cfd_eno3) + u_eno = run_simulation(cfd_eno3, config_eno3.final_time) + u_list.append(u_eno) + + # WENO + + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + domain_weno3 = ComputationalDomain(mesh, config_weno3) + cfd_weno3 = Cfd(config_weno3, domain_weno3) + + init_field(cfd_weno3) + u_weno = run_simulation(cfd_weno3, config_weno3.final_time) + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03a/weno3.py b/example/1d-linear-convection/weno3/python/03a/weno3.py new file mode 100644 index 00000000..8d3f7068 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03a/weno3.py @@ -0,0 +1,625 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + weno3L_periodic( cfd, q, solution.q_face_left ) + weno3R_periodic( cfd, q, solution.q_face_right ) + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, mesh, config): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.mesh = mesh + self.config = config + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, domain): + self.config = config + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + + domain = cfd.domain + solution = cfd.solution + + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + domain_eno3 = ComputationalDomain(mesh, config_eno3) + cfd_eno3 = Cfd(config_eno3, domain_eno3) + + + u_list = [] + # ENO + init_field(cfd_eno3) + u_eno = run_simulation(cfd_eno3, config_eno3.final_time) + u_list.append(u_eno) + + # WENO + + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + domain_weno3 = ComputationalDomain(mesh, config_weno3) + cfd_weno3 = Cfd(config_weno3, domain_weno3) + + init_field(cfd_weno3) + u_weno = run_simulation(cfd_weno3, config_weno3.final_time) + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03b/weno3.py b/example/1d-linear-convection/weno3/python/03b/weno3.py new file mode 100644 index 00000000..48d6c3ea --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03b/weno3.py @@ -0,0 +1,630 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + weno3L_periodic( cfd, q, solution.q_face_left ) + weno3R_periodic( cfd, q, solution.q_face_right ) + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, mesh, config): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.mesh = mesh + self.config = config + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, domain): + self.config = config + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def init_field(self): + domain = self.domain + solution = self.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def run(self): + self.init_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + domain_eno3 = ComputationalDomain(mesh, config_eno3) + cfd_eno3 = Cfd(config_eno3, domain_eno3) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + domain_weno3 = ComputationalDomain(mesh, config_weno3) + cfd_weno3 = Cfd(config_weno3, domain_weno3) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03c/weno3.py b/example/1d-linear-convection/weno3/python/03c/weno3.py new file mode 100644 index 00000000..04db501b --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03c/weno3.py @@ -0,0 +1,616 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L_periodic( cfd, q, solution.q_face_left ) + self.weno3R_periodic( cfd, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L_periodic(self, cfd, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R_periodic(self, cfd, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def init_field(self): + domain = self.domain + solution = self.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def run(self): + self.init_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03d/weno3.py b/example/1d-linear-convection/weno3/python/03d/weno3.py new file mode 100644 index 00000000..3ad59e23 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03d/weno3.py @@ -0,0 +1,613 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = self.wc3L(v1,v2,v3) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = self.wc3R(v1,v2,v3) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def init_field(self): + domain = self.domain + solution = self.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def run(self): + self.init_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03e/weno3.py b/example/1d-linear-convection/weno3/python/03e/weno3.py new file mode 100644 index 00000000..112924be --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03e/weno3.py @@ -0,0 +1,613 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = self.wc3L(v1,v2,v3) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = self.wc3R(v1,v2,v3) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def init_field(self): + domain = self.domain + solution = self.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def run(self): + self.init_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03f/weno3.py b/example/1d-linear-convection/weno3/python/03f/weno3.py new file mode 100644 index 00000000..be29ec56 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03f/weno3.py @@ -0,0 +1,614 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = self.wc3L(v1,v2,v3) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def init_field(self): + domain = self.domain + solution = self.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def run(self): + self.init_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03g/weno3.py b/example/1d-linear-convection/weno3/python/03g/weno3.py new file mode 100644 index 00000000..c8660a84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03g/weno3.py @@ -0,0 +1,645 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def init_field(self): + domain = self.domain + solution = self.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def initial_condition(self, x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + + def init_field_new(self): + domain = self.domain + solution = self.solution + sol = self.initial_condition(domain.mesh.xcc) + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = sol[j] + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def exact_solution(self): + """Analytical solution with periodic boundary conditions""" + x = self.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.mesh.L + x_shifted = (x - c * T + L) % L + return initial_condition(x_shifted) + + def run(self): + #self.init_field() + self.init_field_new() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03h/weno3.py b/example/1d-linear-convection/weno3/python/03h/weno3.py new file mode 100644 index 00000000..052c2eb2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03h/weno3.py @@ -0,0 +1,695 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def apply_boundary_conditions(self, cfd_instance): + """应用边界条件""" + boundary(self.u, cfd_instance) + + def update_old_field(self): + """更新旧场""" + update_oldfield(self.un, self.u) + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(config, domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.solution.apply_boundary_conditions(self) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = cfd_weno3.exact_solution() + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03i/weno3.py b/example/1d-linear-convection/weno3/python/03i/weno3.py new file mode 100644 index 00000000..433134df --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03i/weno3.py @@ -0,0 +1,738 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + residual(self.solution.u, self.cfd) + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + boundary(self.solution.u, self.cfd) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + # RK1核心逻辑:u = u + dt * res + self.compute_residual() # 复用公共残差计算 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() # 复用公共边界条件 + self.solution.update_old_field() # 同步old field + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() # 保存预测值 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred # 更新预测值 + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def apply_boundary_conditions(self, cfd_instance): + """应用边界条件""" + boundary(self.u, cfd_instance) + + def update_old_field(self): + """更新旧场""" + update_oldfield(self.un, self.u) + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(config, domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + # 初始化时间推进器(工厂创建,一行搞定) + self.integrator = TimeIntegratorFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.solution.apply_boundary_conditions(self) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = cfd_weno3.exact_solution() + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04/weno3.py b/example/1d-linear-convection/weno3/python/04/weno3.py new file mode 100644 index 00000000..2ab18139 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04/weno3.py @@ -0,0 +1,795 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + #residual(self.solution.u, self.cfd) + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + boundary(self.solution.u, self.cfd) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + # RK1核心逻辑:u = u + dt * res + self.compute_residual() # 复用公共残差计算 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() # 复用公共边界条件 + self.solution.update_old_field() # 同步old field + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() # 保存预测值 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred # 更新预测值 + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def apply_boundary_conditions(self, cfd_instance): + """应用边界条件""" + boundary(self.u, cfd_instance) + + def update_old_field(self): + """更新旧场""" + update_oldfield(self.un, self.u) + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.solution.apply_boundary_conditions(self) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = cfd_weno3.exact_solution() + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04a/core.py b/example/1d-linear-convection/weno3/python/04a/core.py new file mode 100644 index 00000000..001d5c58 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04a/core.py @@ -0,0 +1,737 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + #residual(self.solution.u, self.cfd) + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + boundary(self.solution.u, self.cfd) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + # RK1核心逻辑:u = u + dt * res + self.compute_residual() # 复用公共残差计算 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() # 复用公共边界条件 + self.solution.update_old_field() # 同步old field + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() # 保存预测值 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred # 更新预测值 + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def apply_boundary_conditions(self, cfd_instance): + """应用边界条件""" + boundary(self.u, cfd_instance) + + def update_old_field(self): + """更新旧场""" + update_oldfield(self.un, self.u) + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.solution.apply_boundary_conditions(self) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04a/plotter.py b/example/1d-linear-convection/weno3/python/04a/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04a/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04a/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04a/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04a/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04b/core.py b/example/1d-linear-convection/weno3/python/04b/core.py new file mode 100644 index 00000000..86441655 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04b/core.py @@ -0,0 +1,810 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + #boundary(self.solution.u, self.cfd) + self.cfd.boundary_condition.apply(self.solution.u) + + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + # RK1核心逻辑:u = u + dt * res + self.compute_residual() # 复用公共残差计算 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() # 复用公共边界条件 + self.solution.update_old_field() # 同步old field + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() # 保存预测值 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred # 更新预测值 + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04b/plotter.py b/example/1d-linear-convection/weno3/python/04b/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04b/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04b/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04b/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04b/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04c/core.py b/example/1d-linear-convection/weno3/python/04c/core.py new file mode 100644 index 00000000..48c09a02 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04c/core.py @@ -0,0 +1,752 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# 新增:从 flux.py 导入通量相关类 +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + #boundary(self.solution.u, self.cfd) + self.cfd.boundary_condition.apply(self.solution.u) + + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + # RK1核心逻辑:u = u + dt * res + self.compute_residual() # 复用公共残差计算 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() # 复用公共边界条件 + self.solution.update_old_field() # 同步old field + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() # 保存预测值 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred # 更新预测值 + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04c/flux.py b/example/1d-linear-convection/weno3/python/04c/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04c/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04c/plotter.py b/example/1d-linear-convection/weno3/python/04c/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04c/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04c/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04c/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04c/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04d/boundary.py b/example/1d-linear-convection/weno3/python/04d/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04d/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04d/core.py b/example/1d-linear-convection/weno3/python/04d/core.py new file mode 100644 index 00000000..0b9cca24 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04d/core.py @@ -0,0 +1,673 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# 已有 flux 导入 +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# 新增 boundary 导入 +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + #boundary(self.solution.u, self.cfd) + self.cfd.boundary_condition.apply(self.solution.u) + + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + # RK1核心逻辑:u = u + dt * res + self.compute_residual() # 复用公共残差计算 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() # 复用公共边界条件 + self.solution.update_old_field() # 同步old field + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() # 保存预测值 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred # 更新预测值 + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04d/flux.py b/example/1d-linear-convection/weno3/python/04d/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04d/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04d/plotter.py b/example/1d-linear-convection/weno3/python/04d/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04d/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04d/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04d/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04d/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04e/boundary.py b/example/1d-linear-convection/weno3/python/04e/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04e/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04e/core.py b/example/1d-linear-convection/weno3/python/04e/core.py new file mode 100644 index 00000000..bb245bf8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04e/core.py @@ -0,0 +1,563 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# 已有 flux 导入 +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# 新增 boundary 导入 +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04e/flux.py b/example/1d-linear-convection/weno3/python/04e/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04e/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04e/plotter.py b/example/1d-linear-convection/weno3/python/04e/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04e/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04e/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04e/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04e/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04e/time_integration.py b/example/1d-linear-convection/weno3/python/04e/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04e/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04f/boundary.py b/example/1d-linear-convection/weno3/python/04f/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04f/core.py b/example/1d-linear-convection/weno3/python/04f/core.py new file mode 100644 index 00000000..96868195 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/core.py @@ -0,0 +1,543 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04f/flux.py b/example/1d-linear-convection/weno3/python/04f/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04f/mesh.py b/example/1d-linear-convection/weno3/python/04f/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04f/plotter.py b/example/1d-linear-convection/weno3/python/04f/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04f/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04f/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04f/time_integration.py b/example/1d-linear-convection/weno3/python/04f/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/boundary.py b/example/1d-linear-convection/weno3/python/04g/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/core.py b/example/1d-linear-convection/weno3/python/04g/core.py new file mode 100644 index 00000000..794fe5cf --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/core.py @@ -0,0 +1,333 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory, init_coef + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04g/flux.py b/example/1d-linear-convection/weno3/python/04g/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/mesh.py b/example/1d-linear-convection/weno3/python/04g/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/plotter.py b/example/1d-linear-convection/weno3/python/04g/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/reconstructor.py b/example/1d-linear-convection/weno3/python/04g/reconstructor.py new file mode 100644 index 00000000..1614cc84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/reconstructor.py @@ -0,0 +1,166 @@ +# reconstructor.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def init_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + + +# ---------------------- 2. 抽象重构器基类 ---------------------- +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +# ---------------------- 4. WENO 重构器(3阶) ---------------------- +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self.weno3L(domain, q, solution.q_face_left) + self.weno3R(domain, q, solution.q_face_right) + + def weno3L(self, domain, u, f): + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3R(v3, v2, v1) + + def weno3R(self, domain, u, f): + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3L(v3, v2, v1) + + def wc3L(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 2.0/3.0, 1.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + return w0 * q0 + w1 * q1 + + def wc3R(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 1.0/3.0, 2.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + return w0 * q0 + w1 * q1 + + +# ---------------------- 5. 重构器工厂 ---------------------- +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + if scheme == "eno": + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04g/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/time_integration.py b/example/1d-linear-convection/weno3/python/04g/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/boundary.py b/example/1d-linear-convection/weno3/python/04h/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/core.py b/example/1d-linear-convection/weno3/python/04h/core.py new file mode 100644 index 00000000..a221aab6 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/core.py @@ -0,0 +1,259 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory, init_coef + +from initial_condition import InitialCondition, StepFunctionIC, SineWaveIC, GaussianPulseIC, InitialConditionFactory + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04h/flux.py b/example/1d-linear-convection/weno3/python/04h/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/initial_condition.py b/example/1d-linear-convection/weno3/python/04h/initial_condition.py new file mode 100644 index 00000000..166b7dbd --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/initial_condition.py @@ -0,0 +1,81 @@ +# initial_condition.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 初始条件抽象基类 ---------------------- +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +# ---------------------- 2. 具体初始条件实现 ---------------------- +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +# ---------------------- 3. 初始条件工厂 ---------------------- +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/mesh.py b/example/1d-linear-convection/weno3/python/04h/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/plotter.py b/example/1d-linear-convection/weno3/python/04h/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/reconstructor.py b/example/1d-linear-convection/weno3/python/04h/reconstructor.py new file mode 100644 index 00000000..1614cc84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/reconstructor.py @@ -0,0 +1,166 @@ +# reconstructor.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def init_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + + +# ---------------------- 2. 抽象重构器基类 ---------------------- +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +# ---------------------- 4. WENO 重构器(3阶) ---------------------- +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self.weno3L(domain, q, solution.q_face_left) + self.weno3R(domain, q, solution.q_face_right) + + def weno3L(self, domain, u, f): + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3R(v3, v2, v1) + + def weno3R(self, domain, u, f): + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3L(v3, v2, v1) + + def wc3L(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 2.0/3.0, 1.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + return w0 * q0 + w1 * q1 + + def wc3R(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 1.0/3.0, 2.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + return w0 * q0 + w1 * q1 + + +# ---------------------- 5. 重构器工厂 ---------------------- +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + if scheme == "eno": + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04h/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/time_integration.py b/example/1d-linear-convection/weno3/python/04h/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/boundary.py b/example/1d-linear-convection/weno3/python/04i/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/core.py b/example/1d-linear-convection/weno3/python/04i/core.py new file mode 100644 index 00000000..12227f4d --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/core.py @@ -0,0 +1,208 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory, init_coef + +from initial_condition import InitialCondition, StepFunctionIC, SineWaveIC, GaussianPulseIC, InitialConditionFactory + +from domain import Domain + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = Domain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04i/domain.py b/example/1d-linear-convection/weno3/python/04i/domain.py new file mode 100644 index 00000000..887191df --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/domain.py @@ -0,0 +1,55 @@ +# domain.py +from mesh import Mesh + +class Domain: + """计算域:管理物理区域、ghost层、索引映射等逻辑,依赖 Mesh 提供几何信息""" + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + # 可选:调试信息(可后续移除) + # print(f"mesh.ncells={mesh.ncells}") + # print(f"self.config.spatial_order={self.config.spatial_order}") + # print(f"self.nghosts={self.nghosts}") + # print(f"self.ist={self.ist}") + # print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + if scheme == "eno": + nghosts = order + elif scheme == "weno": + nghosts = order // 2 + 1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/flux.py b/example/1d-linear-convection/weno3/python/04i/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/initial_condition.py b/example/1d-linear-convection/weno3/python/04i/initial_condition.py new file mode 100644 index 00000000..166b7dbd --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/initial_condition.py @@ -0,0 +1,81 @@ +# initial_condition.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 初始条件抽象基类 ---------------------- +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +# ---------------------- 2. 具体初始条件实现 ---------------------- +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +# ---------------------- 3. 初始条件工厂 ---------------------- +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/mesh.py b/example/1d-linear-convection/weno3/python/04i/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/plotter.py b/example/1d-linear-convection/weno3/python/04i/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/reconstructor.py b/example/1d-linear-convection/weno3/python/04i/reconstructor.py new file mode 100644 index 00000000..1614cc84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/reconstructor.py @@ -0,0 +1,166 @@ +# reconstructor.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def init_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + + +# ---------------------- 2. 抽象重构器基类 ---------------------- +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +# ---------------------- 4. WENO 重构器(3阶) ---------------------- +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self.weno3L(domain, q, solution.q_face_left) + self.weno3R(domain, q, solution.q_face_right) + + def weno3L(self, domain, u, f): + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3R(v3, v2, v1) + + def weno3R(self, domain, u, f): + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3L(v3, v2, v1) + + def wc3L(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 2.0/3.0, 1.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + return w0 * q0 + w1 * q1 + + def wc3R(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 1.0/3.0, 2.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + return w0 * q0 + w1 * q1 + + +# ---------------------- 5. 重构器工厂 ---------------------- +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + if scheme == "eno": + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04i/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/time_integration.py b/example/1d-linear-convection/weno3/python/04i/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/boundary.py b/example/1d-linear-convection/weno3/python/04j/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/core.py b/example/1d-linear-convection/weno3/python/04j/core.py new file mode 100644 index 00000000..3df375a6 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/core.py @@ -0,0 +1,169 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory, init_coef + +from initial_condition import InitialCondition, StepFunctionIC, SineWaveIC, GaussianPulseIC, InitialConditionFactory + +from domain import Domain +from solution import Solution # 👈 新增 + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = Domain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04j/domain.py b/example/1d-linear-convection/weno3/python/04j/domain.py new file mode 100644 index 00000000..887191df --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/domain.py @@ -0,0 +1,55 @@ +# domain.py +from mesh import Mesh + +class Domain: + """计算域:管理物理区域、ghost层、索引映射等逻辑,依赖 Mesh 提供几何信息""" + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + # 可选:调试信息(可后续移除) + # print(f"mesh.ncells={mesh.ncells}") + # print(f"self.config.spatial_order={self.config.spatial_order}") + # print(f"self.nghosts={self.nghosts}") + # print(f"self.ist={self.ist}") + # print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + if scheme == "eno": + nghosts = order + elif scheme == "weno": + nghosts = order // 2 + 1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/flux.py b/example/1d-linear-convection/weno3/python/04j/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/initial_condition.py b/example/1d-linear-convection/weno3/python/04j/initial_condition.py new file mode 100644 index 00000000..166b7dbd --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/initial_condition.py @@ -0,0 +1,81 @@ +# initial_condition.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 初始条件抽象基类 ---------------------- +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +# ---------------------- 2. 具体初始条件实现 ---------------------- +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +# ---------------------- 3. 初始条件工厂 ---------------------- +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/mesh.py b/example/1d-linear-convection/weno3/python/04j/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/plotter.py b/example/1d-linear-convection/weno3/python/04j/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/reconstructor.py b/example/1d-linear-convection/weno3/python/04j/reconstructor.py new file mode 100644 index 00000000..1614cc84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/reconstructor.py @@ -0,0 +1,166 @@ +# reconstructor.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def init_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + + +# ---------------------- 2. 抽象重构器基类 ---------------------- +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +# ---------------------- 4. WENO 重构器(3阶) ---------------------- +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self.weno3L(domain, q, solution.q_face_left) + self.weno3R(domain, q, solution.q_face_right) + + def weno3L(self, domain, u, f): + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3R(v3, v2, v1) + + def weno3R(self, domain, u, f): + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3L(v3, v2, v1) + + def wc3L(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 2.0/3.0, 1.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + return w0 * q0 + w1 * q1 + + def wc3R(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 1.0/3.0, 2.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + return w0 * q0 + w1 * q1 + + +# ---------------------- 5. 重构器工厂 ---------------------- +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + if scheme == "eno": + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04j/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/solution.py b/example/1d-linear-convection/weno3/python/04j/solution.py new file mode 100644 index 00000000..92a46d3f --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/solution.py @@ -0,0 +1,40 @@ +# solution.py +import numpy as np +from initial_condition import InitialConditionFactory + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: Domain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + self.un[:] = self.u[:] \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/time_integration.py b/example/1d-linear-convection/weno3/python/04j/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/boundary.py b/example/1d-linear-convection/weno3/python/04k/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/config.py b/example/1d-linear-convection/weno3/python/04k/config.py new file mode 100644 index 00000000..47f8c769 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/config.py @@ -0,0 +1,39 @@ +# config.py +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/domain.py b/example/1d-linear-convection/weno3/python/04k/domain.py new file mode 100644 index 00000000..887191df --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/domain.py @@ -0,0 +1,55 @@ +# domain.py +from mesh import Mesh + +class Domain: + """计算域:管理物理区域、ghost层、索引映射等逻辑,依赖 Mesh 提供几何信息""" + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + # 可选:调试信息(可后续移除) + # print(f"mesh.ncells={mesh.ncells}") + # print(f"self.config.spatial_order={self.config.spatial_order}") + # print(f"self.nghosts={self.nghosts}") + # print(f"self.ist={self.ist}") + # print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + if scheme == "eno": + nghosts = order + elif scheme == "weno": + nghosts = order // 2 + 1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/flux.py b/example/1d-linear-convection/weno3/python/04k/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/initial_condition.py b/example/1d-linear-convection/weno3/python/04k/initial_condition.py new file mode 100644 index 00000000..166b7dbd --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/initial_condition.py @@ -0,0 +1,81 @@ +# initial_condition.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 初始条件抽象基类 ---------------------- +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +# ---------------------- 2. 具体初始条件实现 ---------------------- +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +# ---------------------- 3. 初始条件工厂 ---------------------- +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/mesh.py b/example/1d-linear-convection/weno3/python/04k/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/plotter.py b/example/1d-linear-convection/weno3/python/04k/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/reconstructor.py b/example/1d-linear-convection/weno3/python/04k/reconstructor.py new file mode 100644 index 00000000..1614cc84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/reconstructor.py @@ -0,0 +1,166 @@ +# reconstructor.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def init_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + + +# ---------------------- 2. 抽象重构器基类 ---------------------- +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +# ---------------------- 4. WENO 重构器(3阶) ---------------------- +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self.weno3L(domain, q, solution.q_face_left) + self.weno3R(domain, q, solution.q_face_right) + + def weno3L(self, domain, u, f): + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3R(v3, v2, v1) + + def weno3R(self, domain, u, f): + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3L(v3, v2, v1) + + def wc3L(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 2.0/3.0, 1.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + return w0 * q0 + w1 * q1 + + def wc3R(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 1.0/3.0, 2.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + return w0 * q0 + w1 * q1 + + +# ---------------------- 5. 重构器工厂 ---------------------- +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + if scheme == "eno": + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/residual.py b/example/1d-linear-convection/weno3/python/04k/residual.py new file mode 100644 index 00000000..b4d4d7dc --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/residual.py @@ -0,0 +1,40 @@ +# residual.py + +from flux import FluxCalculatorFactory + +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + self._reconstruct() + self._compute_inviscid_flux() + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04k/run_eno_weno.py new file mode 100644 index 00000000..31b1215b --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/run_eno_weno.py @@ -0,0 +1,52 @@ +from solver import Cfd +from config import CfdConfig +from mesh import Mesh +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/solution.py b/example/1d-linear-convection/weno3/python/04k/solution.py new file mode 100644 index 00000000..92a46d3f --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/solution.py @@ -0,0 +1,40 @@ +# solution.py +import numpy as np +from initial_condition import InitialConditionFactory + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: Domain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + self.un[:] = self.u[:] \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/solver.py b/example/1d-linear-convection/weno3/python/04k/solver.py new file mode 100644 index 00000000..a6a4ef49 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/solver.py @@ -0,0 +1,89 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory, init_coef + +from initial_condition import InitialCondition, StepFunctionIC, SineWaveIC, GaussianPulseIC, InitialConditionFactory + +from domain import Domain +from solution import Solution # 👈 新增 + +from config import CfdConfig +from residual import ResidualCalculator + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = Domain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04k/time_integration.py b/example/1d-linear-convection/weno3/python/04k/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/boundary.py b/example/1d-linear-convection/weno3/python/04l/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/config.py b/example/1d-linear-convection/weno3/python/04l/config.py new file mode 100644 index 00000000..9ca34391 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/config.py @@ -0,0 +1,39 @@ +# config.py +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = "rusanov" # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/domain.py b/example/1d-linear-convection/weno3/python/04l/domain.py new file mode 100644 index 00000000..887191df --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/domain.py @@ -0,0 +1,55 @@ +# domain.py +from mesh import Mesh + +class Domain: + """计算域:管理物理区域、ghost层、索引映射等逻辑,依赖 Mesh 提供几何信息""" + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + # 可选:调试信息(可后续移除) + # print(f"mesh.ncells={mesh.ncells}") + # print(f"self.config.spatial_order={self.config.spatial_order}") + # print(f"self.nghosts={self.nghosts}") + # print(f"self.ist={self.ist}") + # print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + if scheme == "eno": + nghosts = order + elif scheme == "weno": + nghosts = order // 2 + 1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/flux.py b/example/1d-linear-convection/weno3/python/04l/flux.py new file mode 100644 index 00000000..feaa723a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + "rusanov": RusanovFluxCalculator, + "engquist-osher": EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) diff --git a/example/1d-linear-convection/weno3/python/04l/initial_condition.py b/example/1d-linear-convection/weno3/python/04l/initial_condition.py new file mode 100644 index 00000000..166b7dbd --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/initial_condition.py @@ -0,0 +1,81 @@ +# initial_condition.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 初始条件抽象基类 ---------------------- +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +# ---------------------- 2. 具体初始条件实现 ---------------------- +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +# ---------------------- 3. 初始条件工厂 ---------------------- +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/mesh.py b/example/1d-linear-convection/weno3/python/04l/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/plotter.py b/example/1d-linear-convection/weno3/python/04l/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/reconstructor.py b/example/1d-linear-convection/weno3/python/04l/reconstructor.py new file mode 100644 index 00000000..1614cc84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/reconstructor.py @@ -0,0 +1,166 @@ +# reconstructor.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def init_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + + +# ---------------------- 2. 抽象重构器基类 ---------------------- +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +# ---------------------- 4. WENO 重构器(3阶) ---------------------- +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self.weno3L(domain, q, solution.q_face_left) + self.weno3R(domain, q, solution.q_face_right) + + def weno3L(self, domain, u, f): + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3R(v3, v2, v1) + + def weno3R(self, domain, u, f): + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3L(v3, v2, v1) + + def wc3L(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 2.0/3.0, 1.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + return w0 * q0 + w1 * q1 + + def wc3R(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 1.0/3.0, 2.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + return w0 * q0 + w1 * q1 + + +# ---------------------- 5. 重构器工厂 ---------------------- +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + if scheme == "eno": + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/residual.py b/example/1d-linear-convection/weno3/python/04l/residual.py new file mode 100644 index 00000000..b4d4d7dc --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/residual.py @@ -0,0 +1,40 @@ +# residual.py + +from flux import FluxCalculatorFactory + +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + self._reconstruct() + self._compute_inviscid_flux() + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04l/run_eno_weno.py new file mode 100644 index 00000000..31b1215b --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/run_eno_weno.py @@ -0,0 +1,52 @@ +from solver import Cfd +from config import CfdConfig +from mesh import Mesh +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/solution.py b/example/1d-linear-convection/weno3/python/04l/solution.py new file mode 100644 index 00000000..92a46d3f --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/solution.py @@ -0,0 +1,40 @@ +# solution.py +import numpy as np +from initial_condition import InitialConditionFactory + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: Domain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + self.un[:] = self.u[:] \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/solver.py b/example/1d-linear-convection/weno3/python/04l/solver.py new file mode 100644 index 00000000..a6a4ef49 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/solver.py @@ -0,0 +1,89 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory, init_coef + +from initial_condition import InitialCondition, StepFunctionIC, SineWaveIC, GaussianPulseIC, InitialConditionFactory + +from domain import Domain +from solution import Solution # 👈 新增 + +from config import CfdConfig +from residual import ResidualCalculator + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = Domain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04l/time_integration.py b/example/1d-linear-convection/weno3/python/04l/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/boundary.py b/example/1d-linear-convection/weno3/python/04m/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/config.py b/example/1d-linear-convection/weno3/python/04m/config.py new file mode 100644 index 00000000..b3ad4749 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/config.py @@ -0,0 +1,41 @@ +# config.py +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = "rusanov" # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + self.spatial_order = 2 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme.startswith("weno"): + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/domain.py b/example/1d-linear-convection/weno3/python/04m/domain.py new file mode 100644 index 00000000..ae1cca4e --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/domain.py @@ -0,0 +1,56 @@ +# domain.py +from mesh import Mesh + +class Domain: + """计算域:管理物理区域、ghost层、索引映射等逻辑,依赖 Mesh 提供几何信息""" + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + # 可选:调试信息(可后续移除) + # print(f"mesh.ncells={mesh.ncells}") + # print(f"self.config.spatial_order={self.config.spatial_order}") + # print(f"self.nghosts={self.nghosts}") + # print(f"self.ist={self.ist}") + # print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme.lower() + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # ✅ 统一处理:所有以 "weno" 开头的都按 WENO 规则计算 ghost 层数 + if scheme == "eno": + nghosts = order + elif scheme.startswith("weno"): # ← 关键修改:支持 "weno", "weno3", "weno5", "weno-z" 等 + nghosts = order // 2 + 1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/flux.py b/example/1d-linear-convection/weno3/python/04m/flux.py new file mode 100644 index 00000000..feaa723a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + "rusanov": RusanovFluxCalculator, + "engquist-osher": EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) diff --git a/example/1d-linear-convection/weno3/python/04m/initial_condition.py b/example/1d-linear-convection/weno3/python/04m/initial_condition.py new file mode 100644 index 00000000..166b7dbd --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/initial_condition.py @@ -0,0 +1,81 @@ +# initial_condition.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 初始条件抽象基类 ---------------------- +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +# ---------------------- 2. 具体初始条件实现 ---------------------- +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +# ---------------------- 3. 初始条件工厂 ---------------------- +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/mesh.py b/example/1d-linear-convection/weno3/python/04m/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/plotter.py b/example/1d-linear-convection/weno3/python/04m/plotter.py new file mode 100644 index 00000000..9f1a414f --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure("OneFLOW-CFD Solver",figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure("OneFLOW-CFD Solver",figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/reconstructor/__init__.py b/example/1d-linear-convection/weno3/python/04m/reconstructor/__init__.py new file mode 100644 index 00000000..fa17547a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/reconstructor/__init__.py @@ -0,0 +1,4 @@ +# reconstructor/__init__.py +from .factory import ReconstructorFactory +from .base import Reconstructor +# 可选择性导出具体类(通常不需要) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/reconstructor/base.py b/example/1d-linear-convection/weno3/python/04m/reconstructor/base.py new file mode 100644 index 00000000..3cb4763d --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/reconstructor/base.py @@ -0,0 +1,7 @@ +# reconstructor/base.py +from abc import ABC, abstractmethod + +class Reconstructor(ABC): + @abstractmethod + def reconstruct(self, q, cfd): + pass \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/reconstructor/eno.py b/example/1d-linear-convection/weno3/python/04m/reconstructor/eno.py new file mode 100644 index 00000000..e087fe57 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/reconstructor/eno.py @@ -0,0 +1,88 @@ +# reconstructor/eno.py +import numpy as np +from .base import Reconstructor # 👈 正确导入基类 + + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def _init_eno_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + _init_eno_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/reconstructor/factory.py b/example/1d-linear-convection/weno3/python/04m/reconstructor/factory.py new file mode 100644 index 00000000..bf5795bc --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/reconstructor/factory.py @@ -0,0 +1,35 @@ +# reconstructor/factory.py +from .eno import EnoReconstructor +from .weno3 import Weno3Reconstructor + +class ReconstructorFactory: + _schemes = { + "eno": EnoReconstructor, + "weno3": Weno3Reconstructor, # ← 注意:这里用 "weno3" + # "weno5": Weno5Reconstructor, # 未来扩展 + } + + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + order = getattr(config, 'spatial_order', None) + + # ✅ 关键:将 "weno" 自动映射为 "weno3", "weno5" 等 + if scheme == "weno": + if order is None: + raise ValueError("使用 'weno' 时必须设置 config.spatial_order") + scheme = f"weno{order}" + + # 检查是否支持 + if scheme not in ReconstructorFactory._schemes: + supported = list(ReconstructorFactory._schemes.keys()) + raise ValueError(f"不支持的重建格式:'{scheme}'(支持:{supported})") + + recon_cls = ReconstructorFactory._schemes[scheme] + + # 根据 scheme 类型创建实例 + if scheme.startswith("eno"): + return recon_cls(order, domain.ntcells) + elif scheme.startswith("weno"): + return recon_cls() # WENO 类通常无参 + # 可扩展 elif... diff --git a/example/1d-linear-convection/weno3/python/04m/reconstructor/weno3.py b/example/1d-linear-convection/weno3/python/04m/reconstructor/weno3.py new file mode 100644 index 00000000..3d440309 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/reconstructor/weno3.py @@ -0,0 +1,56 @@ +# reconstructor/weno3.py +import numpy as np +from .base import Reconstructor + +class Weno3Reconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self._reconstruct_left_interfaces(domain, q, solution.q_face_left) + self._reconstruct_right_interfaces(domain, q, solution.q_face_right) + + def _reconstruct_left_interfaces(self, domain, u, qL): + """在每个 i+1/2 界面,计算左单元贡献的 qL (即 u_{i+1/2}^-)""" + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1, v2, v3 = u[i-1], u[i], u[i+1] + qL[j] = self._reconstruct_from_right_biased_stencil(v3, v2, v1) + + def _reconstruct_right_interfaces(self, domain, u, qR): + """在每个 i+1/2 界面,计算右单元贡献的 qR (即 u_{i+1/2}^+)""" + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1, v2, v3 = u[i-1], u[i], u[i+1] + qR[j] = self._reconstruct_from_left_biased_stencil(v3, v2, v1) + + def _reconstruct_from_left_biased_stencil(self, v1, v2, v3): + """使用左偏 stencil (v1,v2,v3) 重建界面值(对应 u_{i+1/2}^+)""" + eps = 1e-6 + beta0 = (v3 - v2)**2 # smoothness indicator for stencil [v2, v3] + beta1 = (v2 - v1)**2 # smoothness indicator for stencil [v1, v2] + + d0, d1 = 2/3, 1/3 # optimal linear weights (for right value) + alpha0 = d0 / (eps + beta0)**2 + alpha1 = d1 / (eps + beta1)**2 + w0 = alpha0 / (alpha0 + alpha1) + w1 = alpha1 / (alpha0 + alpha1) + + q0 = 1.5 * v2 - 0.5 * v3 # reconstruction from [v2, v3] + q1 = 0.5 * v1 + 0.5 * v2 # reconstruction from [v1, v2] + return w0 * q0 + w1 * q1 + + def _reconstruct_from_right_biased_stencil(self, v1, v2, v3): + """使用右偏 stencil (v1,v2,v3) 重建界面值(对应 u_{i+1/2}^-)""" + eps = 1e-6 + beta0 = (v3 - v2)**2 + beta1 = (v2 - v1)**2 + + d0, d1 = 1/3, 2/3 # optimal linear weights (for left value) + alpha0 = d0 / (eps + beta0)**2 + alpha1 = d1 / (eps + beta1)**2 + w0 = alpha0 / (alpha0 + alpha1) + w1 = alpha1 / (alpha0 + alpha1) + + q0 = 1.5 * v2 - 0.5 * v3 # from [v2, v3] + q1 = 0.5 * v1 + 0.5 * v2 # from [v1, v2] + return w0 * q0 + w1 * q1 diff --git a/example/1d-linear-convection/weno3/python/04m/residual.py b/example/1d-linear-convection/weno3/python/04m/residual.py new file mode 100644 index 00000000..b4d4d7dc --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/residual.py @@ -0,0 +1,40 @@ +# residual.py + +from flux import FluxCalculatorFactory + +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + self._reconstruct() + self._compute_inviscid_flux() + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04m/run_eno_weno.py new file mode 100644 index 00000000..fd7f3874 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/run_eno_weno.py @@ -0,0 +1,52 @@ +from solver import Cfd +from config import CfdConfig +from mesh import Mesh +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 2 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 2 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/solution.py b/example/1d-linear-convection/weno3/python/04m/solution.py new file mode 100644 index 00000000..92a46d3f --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/solution.py @@ -0,0 +1,40 @@ +# solution.py +import numpy as np +from initial_condition import InitialConditionFactory + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: Domain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + self.un[:] = self.u[:] \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/solver.py b/example/1d-linear-convection/weno3/python/04m/solver.py new file mode 100644 index 00000000..b2024d2c --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/solver.py @@ -0,0 +1,90 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +#from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory +from reconstructor import ReconstructorFactory + +from initial_condition import InitialCondition, StepFunctionIC, SineWaveIC, GaussianPulseIC, InitialConditionFactory + +from domain import Domain +from solution import Solution # 👈 新增 + +from config import CfdConfig +from residual import ResidualCalculator + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = Domain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04m/time_integration.py b/example/1d-linear-convection/weno3/python/04m/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/figure/1d/03u/cfd.png b/example/figure/1d/03u/cfd.png deleted file mode 100644 index c985f2cb6b9acb0de4cc74af8545049db433d5a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32092 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 diff --git a/example/figure/1d/04c/cfd.png b/example/figure/1d/04c/cfd.png deleted file mode 100644 index d464467d8911b1c90a3af87f0f5ca2117edffd76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63137 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', + color='red', + lw=3, + shrinkA=0, shrinkB=0)) + +# 或者使用 fancy 箭头样式 +ax.annotate('向上箭头', xy=(0.5, 0.8), xytext=(0.5, 0.2), + arrowprops=dict(arrowstyle='fancy', + fc='green', ec='darkgreen', + connectionstyle="arc3,rad=0")) + +ax.set_xlim(0, 1) +ax.set_ylim(0, 1) +plt.title('向上的箭头 - plt.annotate()') +plt.grid(True, alpha=0.3) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/arrow/01b/testprj.py b/example/figure/1d/arrow/01b/testprj.py new file mode 100644 index 00000000..06d09b31 --- /dev/null +++ b/example/figure/1d/arrow/01b/testprj.py @@ -0,0 +1,26 @@ +import matplotlib.pyplot as plt + +fig, ax = plt.subplots(figsize=(10, 8)) + +# 不同样式的向上箭头示例 +arrow_styles = [ + ('简单箭头', (0.2, 0.2), '->', 'blue'), + ('燕尾箭头', (0.4, 0.2), '<->', 'red'), + ('填充箭头', (0.6, 0.2), '-|>', 'green'), + ('宽头箭头', (0.8, 0.2), 'wedge,tail_width=0.7', 'purple') +] + +for label, start_pos, style, color in arrow_styles: + x, y = start_pos + ax.annotate(label, xy=(x, y+0.6), xytext=(x, y), + arrowprops=dict(arrowstyle=style, + color=color, + lw=2, + shrinkA=5, shrinkB=5)) + ax.text(x-0.08, y-0.05, f'起点: ({x},{y})', fontsize=8) + +ax.set_xlim(0, 1) +ax.set_ylim(0, 1) +plt.title('不同样式的向上箭头') +plt.grid(True, alpha=0.3) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/arrow/01c/testprj.py b/example/figure/1d/arrow/01c/testprj.py new file mode 100644 index 00000000..6ccfff05 --- /dev/null +++ b/example/figure/1d/arrow/01c/testprj.py @@ -0,0 +1,38 @@ +import matplotlib.pyplot as plt +import numpy as np + +# Create a figure +fig, ax = plt.subplots(figsize=(8, 6)) + +# Draw a line segment +x = [0, 3, 5, 2] +y = [0, 2, 1, 3] +ax.plot(x, y, 'gray', alpha=0.5, linewidth=2, label='Original line') + +# Add arrows on each segment of the line +for i in range(len(x)-1): + # Calculate the midpoint of the segment + mid_x = (x[i] + x[i+1]) / 2 + mid_y = (y[i] + y[i+1]) / 2 + + # Calculate the direction vector + dx = x[i+1] - x[i] + dy = y[i+1] - y[i] + + # Add arrow at the midpoint + ax.annotate('', xy=(mid_x + dx/4, mid_y + dy/4), + xytext=(mid_x - dx/4, mid_y - dy/4), + arrowprops=dict(arrowstyle='->', color=f'C{i}', + lw=2, mutation_scale=15)) + +# Add markers at the endpoints of the line segment +ax.scatter(x, y, c='red', s=100, zorder=5, label='Endpoints') + +# Set figure properties +ax.set_xlim(-1, 6) +ax.set_ylim(-1, 4) +ax.set_aspect('equal') +ax.grid(True, alpha=0.3) +ax.legend() +plt.title('Arrow Example on Line Segment') +plt.show() \ No newline at end of file diff --git a/example/figure/1d/arrow/01d/testprj.py b/example/figure/1d/arrow/01d/testprj.py new file mode 100644 index 00000000..c5ed5c97 --- /dev/null +++ b/example/figure/1d/arrow/01d/testprj.py @@ -0,0 +1,97 @@ +import matplotlib.pyplot as plt +import numpy as np + +# Create figure and axis +fig, ax = plt.subplots(figsize=(8, 6)) + +# Define line segment coordinates +x_start, y_start = 0, 0 # Starting point +x_end, y_end = 5, 3 # Ending point + +# Draw the original line segment +ax.plot([x_start, x_end], [y_start, y_end], + 'gray', alpha=0.5, linewidth=3, label='Original line segment') + +# Calculate direction vector +dx = x_end - x_start +dy = y_end - y_start + +# Calculate normalized direction vector +length = np.sqrt(dx**2 + dy**2) +dx_norm = dx / length +dy_norm = dy / length + +# Define positions where to place arrows along the line +# Using fractions of the line length +arrow_positions = [0.2, 0.5, 0.8] # At 20%, 50%, and 80% of the line + +# Add arrows at specified positions +for i, fraction in enumerate(arrow_positions): + # Calculate arrow position + arrow_x = x_start + fraction * dx + arrow_y = y_start + fraction * dy + + # Calculate arrow endpoints (start and end of arrow) + arrow_length = 0.8 # Length of arrow relative to line + + # Arrow start point (slightly behind the position) + arrow_start_x = arrow_x - 0.3 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.3 * arrow_length * dy_norm + + # Arrow end point (slightly ahead of the position) + arrow_end_x = arrow_x + 0.5 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.5 * arrow_length * dy_norm + + # Draw arrow using annotate + ax.annotate('', # Empty text (only arrow) + xy=(arrow_end_x, arrow_end_y), # Arrow head position + xytext=(arrow_start_x, arrow_start_y), # Arrow tail position + arrowprops=dict(arrowstyle='->', + color=f'C{i}', # Different color for each arrow + linewidth=2.5, + mutation_scale=20, # Controls arrow head size + shrinkA=0, # No shrink at start + shrinkB=0), # No shrink at end + zorder=5) # Ensure arrows are on top + + # Add a small dot at arrow position for clarity + ax.scatter(arrow_x, arrow_y, color=f'C{i}', s=50, + edgecolor='black', linewidth=1, zorder=6, + label=f'Arrow {i+1} position') + +# Add markers at the endpoints of the line +ax.scatter([x_start, x_end], [y_start, y_end], + c='red', s=100, zorder=5, + edgecolor='black', linewidth=2, + label='Endpoints') + +# Add coordinate labels for endpoints +ax.text(x_start, y_start - 0.3, f'({x_start}, {y_start})', + ha='center', va='top', fontsize=10) +ax.text(x_end, y_end + 0.3, f'({x_end}, {y_end})', + ha='center', va='bottom', fontsize=10) + +# Set axis properties +ax.set_xlim(-1, 6) +ax.set_ylim(-1, 4) +ax.set_aspect('equal') # Keep aspect ratio 1:1 +ax.grid(True, alpha=0.3, linestyle='--') + +# Add title and labels +ax.set_title('Multiple Arrows on a Line Segment', fontsize=14, fontweight='bold') +ax.set_xlabel('X-axis', fontsize=12) +ax.set_ylabel('Y-axis', fontsize=12) + +# Add legend +ax.legend(loc='upper left', fontsize=10) + +# Add text explanation +info_text = f"Line length: {length:.2f} units\n" +info_text += f"Direction vector: ({dx}, {dy})\n" +info_text += f"Normalized direction: ({dx_norm:.2f}, {dy_norm:.2f})" +ax.text(0.02, 0.98, info_text, transform=ax.transAxes, + fontsize=9, verticalalignment='top', + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue", alpha=0.8)) + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/example/figure/1d/arrow/01e/testprj.py b/example/figure/1d/arrow/01e/testprj.py new file mode 100644 index 00000000..9605b520 --- /dev/null +++ b/example/figure/1d/arrow/01e/testprj.py @@ -0,0 +1,266 @@ +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_on_line(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, label=None, zorder=2): + """ + Draw an arrow on a line segment at a specified position. + + Parameters: + ----------- + ax : matplotlib.axes.Axes + The axes to draw on + x_start, y_start : float + Starting point coordinates + x_end, y_end : float + Ending point coordinates + position : float (default=0.5) + Position along the line where to place the arrow (0=start, 1=end) + arrow_style : str (default='->') + Arrow style (e.g., '->', '-|>', '<-', '<->', 'fancy') + color : str or tuple (default='blue') + Arrow color + linewidth : float (default=2) + Arrow line width + head_size : float (default=15) + Arrow head size (mutation_scale parameter) + label : str or None (default=None) + Label for the arrow (for legend) + zorder : int (default=2) + Drawing order (higher values are drawn on top) + + Returns: + -------- + arrow_annotation : matplotlib.text.Annotation + The arrow annotation object + """ + # Validate position parameter + if position < 0 or position > 1: + raise ValueError("Position must be between 0 and 1") + + # Calculate the coordinates for the arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction vector + dx = x_end - x_start + dy = y_end - y_start + + # Normalize direction vector + length = np.sqrt(dx**2 + dy**2) + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Define arrow length (relative to line length) + arrow_length = 0.2 * length # 20% of line length + + # Calculate arrow start and end points + # Arrow points in the direction of the line + arrow_start_x = arrow_x - 0.4 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.4 * arrow_length * dy_norm + arrow_end_x = arrow_x + 0.6 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.6 * arrow_length * dy_norm + + # Draw the arrow + arrow = ax.annotate('', + xy=(arrow_end_x, arrow_end_y), + xytext=(arrow_start_x, arrow_start_y), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + shrinkA=0, + shrinkB=0), + zorder=zorder) + + # Add a small marker at the arrow position + marker = ax.scatter(arrow_x, arrow_y, color=color, s=30, + zorder=zorder+1, alpha=0.6) + + # Add label if provided + if label: + ax.text(arrow_x, arrow_y + 0.05*length, label, + color=color, fontsize=9, ha='center', va='bottom', + bbox=dict(boxstyle="round,pad=0.2", facecolor="white", alpha=0.8)) + + return arrow, marker + +# Example usage +def example_usage(): + """Example demonstrating the draw_arrow_on_line function.""" + + # Create figure + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) + + # Example 1: Single line with arrows at different positions + ax1.set_title("Arrows at Different Positions", fontsize=14, fontweight='bold') + + # Draw the line + line_x = [0, 5] + line_y = [0, 3] + ax1.plot(line_x, line_y, 'gray', linewidth=3, alpha=0.3, label='Base line') + + # Draw arrows at different positions + positions = [0.0, 0.25, 0.5, 0.75, 1.0] + colors = ['red', 'orange', 'green', 'blue', 'purple'] + labels = ['Start (0.0)', '0.25', 'Middle (0.5)', '0.75', 'End (1.0)'] + + for pos, color, label in zip(positions, colors, labels): + draw_arrow_on_line(ax1, + x_start=line_x[0], y_start=line_y[0], + x_end=line_x[1], y_end=line_y[1], + position=pos, + arrow_style='->', + color=color, + linewidth=2, + head_size=15, + label=label, + zorder=5) + + # Mark endpoints + ax1.scatter(line_x, line_y, c='black', s=100, zorder=10, label='Endpoints') + + # Add coordinate labels + ax1.text(line_x[0], line_y[0]-0.4, f'({line_x[0]}, {line_y[0]})', + ha='center', fontsize=10) + ax1.text(line_x[1], line_y[1]+0.4, f'({line_x[1]}, {line_y[1]})', + ha='center', fontsize=10) + + ax1.set_xlim(-1, 6) + ax1.set_ylim(-1, 5) + ax1.set_aspect('equal') + ax1.grid(True, alpha=0.3) + ax1.legend(loc='upper left') + ax1.set_xlabel('X-axis') + ax1.set_ylabel('Y-axis') + + # Example 2: Multiple lines with arrows + ax2.set_title("Multiple Lines with Arrows", fontsize=14, fontweight='bold') + + # Define multiple lines + lines = [ + {'start': (0, 0), 'end': (4, 1), 'color': 'blue', 'label': 'Line 1'}, + {'start': (1, 0), 'end': (3, 3), 'color': 'green', 'label': 'Line 2'}, + {'start': (0, 2), 'end': (5, 0), 'color': 'red', 'label': 'Line 3'}, + ] + + # Draw each line with arrows + for i, line in enumerate(lines): + x1, y1 = line['start'] + x2, y2 = line['end'] + color = line['color'] + label = line['label'] + + # Draw the line + ax2.plot([x1, x2], [y1, y2], color=color, linewidth=3, + alpha=0.3, label=label) + + # Draw arrows at three positions + for pos in [0.2, 0.5, 0.8]: + draw_arrow_on_line(ax2, + x_start=x1, y_start=y1, + x_end=x2, y_end=y2, + position=pos, + arrow_style='-|>', + color=color, + linewidth=1.5, + head_size=12, + zorder=5) + + # Mark endpoints + ax2.scatter([x1, x2], [y1, y2], c=color, s=80, zorder=10, edgecolors='black') + + ax2.set_xlim(-0.5, 5.5) + ax2.set_ylim(-0.5, 3.5) + ax2.set_aspect('equal') + ax2.grid(True, alpha=0.3) + ax2.legend(loc='upper right') + ax2.set_xlabel('X-axis') + ax2.set_ylabel('Y-axis') + + plt.tight_layout() + plt.show() + +# Example 3: Interactive demonstration +def interactive_demo(): + """Interactive demonstration with user-specified position.""" + + fig, ax = plt.subplots(figsize=(10, 8)) + + # Define a line + x_start, y_start = 1, 1 + x_end, y_end = 8, 6 + + # Draw the base line + ax.plot([x_start, x_end], [y_start, y_end], + 'gray', linewidth=4, alpha=0.2, label='Base line') + + # Test different arrow styles at position 0.5 + arrow_styles = ['->', '-|>', '<-', '<->', 'fancy', 'simple'] + colors = plt.cm.Set2(np.linspace(0, 1, len(arrow_styles))) + + for i, (style, color) in enumerate(zip(arrow_styles, colors)): + # Offset each arrow slightly for visibility + offset = 0.1 * i + position = 0.3 + offset + + if position <= 1.0: # Ensure position is valid + arrow, marker = draw_arrow_on_line(ax, + x_start=x_start, y_start=y_start, + x_end=x_end, y_end=y_end, + position=position, + arrow_style=style, + color=color, + linewidth=2, + head_size=15, + label=f"style='{style}'", + zorder=5) + + # Mark endpoints + ax.scatter([x_start, x_end], [y_start, y_end], + c='black', s=150, zorder=10, label='Endpoints') + + # Add coordinate labels + ax.text(x_start, y_start-0.5, f'Start: ({x_start}, {y_start})', + ha='center', fontsize=11, fontweight='bold') + ax.text(x_end, y_end+0.5, f'End: ({x_end}, {y_end})', + ha='center', fontsize=11, fontweight='bold') + + # Calculate and display line information + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + info_text = f"Line Information:\n" + info_text += f"• Length: {length:.2f} units\n" + info_text += f"• Direction: ({dx:.1f}, {dy:.1f})\n" + info_text += f"• Angle: {np.degrees(np.arctan2(dy, dx)):.1f}°" + + ax.text(0.02, 0.98, info_text, transform=ax.transAxes, + fontsize=11, verticalalignment='top', + bbox=dict(boxstyle="round,pad=0.5", facecolor="lightyellow", alpha=0.9)) + + # Set plot properties + ax.set_xlim(0, 9) + ax.set_ylim(0, 8) + ax.set_aspect('equal') + ax.grid(True, alpha=0.3, linestyle='--') + ax.set_title("Arrow Styles at Different Positions", fontsize=16, fontweight='bold') + ax.set_xlabel('X-axis', fontsize=12) + ax.set_ylabel('Y-axis', fontsize=12) + ax.legend(loc='lower right', fontsize=10) + + plt.tight_layout() + plt.show() + +# Run examples +if __name__ == "__main__": + print("Example 1: Arrows at different positions") + example_usage() + + print("\nExample 2: Different arrow styles") + interactive_demo() \ No newline at end of file diff --git a/example/figure/1d/arrow/01f/testprj.py b/example/figure/1d/arrow/01f/testprj.py new file mode 100644 index 00000000..b03a8a58 --- /dev/null +++ b/example/figure/1d/arrow/01f/testprj.py @@ -0,0 +1,120 @@ +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_on_line(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, label=None, zorder=2): + """ + Draw an arrow on a line segment at a specified position. + + Parameters: + ----------- + ax : matplotlib.axes.Axes + The axes to draw on + x_start, y_start : float + Starting point coordinates + x_end, y_end : float + Ending point coordinates + position : float (default=0.5) + Position along the line where to place the arrow (0=start, 1=end) + arrow_style : str (default='->') + Arrow style (e.g., '->', '-|>', '<-', '<->', 'fancy') + color : str or tuple (default='blue') + Arrow color + linewidth : float (default=2) + Arrow line width + head_size : float (default=15) + Arrow head size (mutation_scale parameter) + label : str or None (default=None) + Label for the arrow (for legend) + zorder : int (default=2) + Drawing order (higher values are drawn on top) + + Returns: + -------- + arrow_annotation : matplotlib.text.Annotation + The arrow annotation object + """ + # Validate position parameter + if position < 0 or position > 1: + raise ValueError("Position must be between 0 and 1") + + # Calculate the coordinates for the arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction vector + dx = x_end - x_start + dy = y_end - y_start + + # Normalize direction vector + length = np.sqrt(dx**2 + dy**2) + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Define arrow length (relative to line length) + arrow_length = 0.2 * length # 20% of line length + + # Calculate arrow start and end points + # Arrow points in the direction of the line + arrow_start_x = arrow_x - 0.4 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.4 * arrow_length * dy_norm + arrow_end_x = arrow_x + 0.4 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.4 * arrow_length * dy_norm + + # Draw the arrow + arrow = ax.annotate('', + xy=(arrow_end_x, arrow_end_y), + xytext=(arrow_start_x, arrow_start_y), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + shrinkA=0, + shrinkB=0), + zorder=zorder) + + # Add a small marker at the arrow position + marker = ax.scatter(arrow_x, arrow_y, color=color, s=30, + zorder=zorder+1, alpha=0.6) + + # Add label if provided + if label: + ax.text(arrow_x, arrow_y + 0.05*length, label, + color=color, fontsize=9, ha='center', va='bottom', + bbox=dict(boxstyle="round,pad=0.2", facecolor="white", alpha=0.8)) + + return arrow, marker + +# Example usage +def example_usage(): + """Example demonstrating the draw_arrow_on_line function.""" + + # Create figure + plt.figure(figsize=(8, 8)) + + # Draw the line + line_x = [0, 1] + line_y = [0, 1] + plt.plot(line_x, line_y, 'gray', linewidth=3, alpha=0.3, label='Base line') + + draw_arrow_on_line(plt, + x_start=line_x[0], y_start=line_y[0], + x_end=line_x[1], y_end=line_y[1] + ) + + + plt.xlim(-0.5, 5.5) + plt.ylim(-0.5, 3.5) + plt.axis('equal') + plt.grid(True, alpha=0.3) + plt.legend(loc='upper right') + + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + example_usage() diff --git a/example/figure/1d/arrow/01f0/testprj.py b/example/figure/1d/arrow/01f0/testprj.py new file mode 100644 index 00000000..58fefef5 --- /dev/null +++ b/example/figure/1d/arrow/01f0/testprj.py @@ -0,0 +1,110 @@ +import matplotlib.pyplot as plt +import numpy as np + +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_on_line(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, zorder=2, show_connection=False): + """ + Draw an arrow on a line segment at a specified position. + + Parameters: + ----------- + ax : matplotlib.axes.Axes + The axes to draw on + x_start, y_start : float + Starting point coordinates + x_end, y_end : float + Ending point coordinates + position : float (default=0.5) + Position along the line where to place the arrow (0=start, 1=end) + arrow_style : str (default='->') + Arrow style (e.g., '->', '-|>', '<-', '<->', 'fancy') + color : str or tuple (default='blue') + Arrow color + linewidth : float (default=2) + Arrow line width + head_size : float (default=15) + Arrow head size (mutation_scale parameter) + zorder : int (default=2) + Drawing order (higher values are drawn on top) + show_connection : bool (default=False) + Whether to show the connection line (arrow shaft) + """ + # Calculate the coordinates for the arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction vector + dx = x_end - x_start + dy = y_end - y_start + + # Normalize direction vector + length = np.sqrt(dx**2 + dy**2) + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Define arrow length + arrow_length = 0.2 * length + + # Calculate arrow start and end points + arrow_start_x = arrow_x - 0.4 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.4 * arrow_length * dy_norm + arrow_end_x = arrow_x + 0.4 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.4 * arrow_length * dy_norm + + # Create arrow properties + arrow_props = dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + shrinkA=0, + shrinkB=0) + + # If we don't want the connection line, set linestyle to 'none' + if not show_connection: + arrow_props['linestyle'] = 'none' + + # Draw the arrow + arrow = ax.annotate('', + xy=(arrow_end_x, arrow_end_y), + xytext=(arrow_start_x, arrow_start_y), + arrowprops=arrow_props, + zorder=zorder) + + return arrow + +# Example usage +def example_usage(): + """Example demonstrating the draw_arrow_on_line function.""" + + # Create figure + plt.figure(figsize=(8, 8)) + + # Draw the line + line_x = [0, 1] + line_y = [0, 1] + plt.plot(line_x, line_y, 'gray', linewidth=3, alpha=0.3, label='Base line') + + draw_arrow_on_line(plt, + x_start=line_x[0], y_start=line_y[0], + x_end=line_x[1], y_end=line_y[1] + ) + + + plt.xlim(-0.5, 5.5) + plt.ylim(-0.5, 3.5) + plt.axis('equal') + plt.grid(True, alpha=0.3) + plt.legend(loc='upper right') + + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + example_usage() diff --git a/example/figure/1d/arrow/01f1/testprj.py b/example/figure/1d/arrow/01f1/testprj.py new file mode 100644 index 00000000..2c066b4a --- /dev/null +++ b/example/figure/1d/arrow/01f1/testprj.py @@ -0,0 +1,73 @@ +import matplotlib.pyplot as plt +import numpy as np + +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow +# Example usage +def example_usage(): + """Example demonstrating the draw_arrow_on_line function.""" + + # Create figure + plt.figure(figsize=(8, 8)) + + # Draw the line + line_x = [0, 1] + line_y = [0, 1] + plt.plot(line_x, line_y, 'gray', linewidth=3, alpha=0.3, label='Base line') + + draw_arrow_only(plt, + x_start=line_x[0], y_start=line_y[0], + x_end=line_x[1], y_end=line_y[1] + ) + + + plt.xlim(-0.5, 5.5) + plt.ylim(-0.5, 3.5) + plt.axis('equal') + plt.grid(True, alpha=0.3) + plt.legend(loc='upper right') + + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + example_usage() diff --git a/example/figure/1d/arrow/01g/testprj.py b/example/figure/1d/arrow/01g/testprj.py new file mode 100644 index 00000000..779623f9 --- /dev/null +++ b/example/figure/1d/arrow/01g/testprj.py @@ -0,0 +1,279 @@ +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_on_line(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, label=None, zorder=2, show_marker=False): + """ + Draw an arrow on a line segment at a specified position. + + Parameters: + ----------- + ax : matplotlib.axes.Axes + The axes to draw on + x_start, y_start : float + Starting point coordinates + x_end, y_end : float + Ending point coordinates + position : float (default=0.5) + Position along the line where to place the arrow (0=start, 1=end) + arrow_style : str (default='->') + Arrow style (e.g., '->', '-|>', '<-', '<->', 'fancy') + color : str or tuple (default='blue') + Arrow color + linewidth : float (default=2) + Arrow line width + head_size : float (default=15) + Arrow head size (mutation_scale parameter) + label : str or None (default=None) + Label for the arrow (for legend) + zorder : int (default=2) + Drawing order (higher values are drawn on top) + show_marker : bool (default=False) + Whether to show a marker at the arrow position + + Returns: + -------- + arrow_annotation : matplotlib.text.Annotation + The arrow annotation object + """ + # Validate position parameter + if position < 0 or position > 1: + raise ValueError("Position must be between 0 and 1") + + # Calculate the coordinates for the arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction vector + dx = x_end - x_start + dy = y_end - y_start + + # Normalize direction vector + length = np.sqrt(dx**2 + dy**2) + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Define arrow length (relative to line length) + arrow_length = 0.2 * length # 20% of line length + + # Calculate arrow start and end points + # Arrow points in the direction of the line + arrow_start_x = arrow_x - 0.4 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.4 * arrow_length * dy_norm + arrow_end_x = arrow_x + 0.4 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.4 * arrow_length * dy_norm + + # Draw the arrow + arrow = ax.annotate('', + xy=(arrow_end_x, arrow_end_y), + xytext=(arrow_start_x, arrow_start_y), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + shrinkA=0, + shrinkB=0), + zorder=zorder) + + # Add a small marker at the arrow position only if requested + marker = None + if show_marker: + marker = ax.scatter(arrow_x, arrow_y, color=color, s=30, + zorder=zorder+1, alpha=0.6) + + # Add label if provided + if label: + ax.text(arrow_x, arrow_y + 0.05*length, label, + color=color, fontsize=9, ha='center', va='bottom', + bbox=dict(boxstyle="round,pad=0.2", facecolor="white", alpha=0.8)) + + return arrow, marker + +# 简洁版本:完全不绘制标记点 +def draw_arrow_on_line_simple(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, zorder=2): + """ + Simplified version - draw arrow without marker or label. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Arrow length + arrow_length = 0.2 * length + + # Arrow start and end + arrow_start_x = arrow_x - 0.4 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.4 * arrow_length * dy_norm + arrow_end_x = arrow_x + 0.4 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.4 * arrow_length * dy_norm + + # Draw arrow + arrow = ax.annotate('', + xy=(arrow_end_x, arrow_end_y), + xytext=(arrow_start_x, arrow_start_y), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +# Example usage +def example_usage(): + """Example demonstrating different options.""" + + fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5)) + + # 定义线段 + x_start, y_start = 0, 0 + x_end, y_end = 1, 1 + + # 示例1: 不显示标记点(默认) + ax1.set_title("Without marker (default)", fontsize=12) + ax1.plot([x_start, x_end], [y_start, y_end], 'gray', linewidth=2, alpha=0.3) + + # 绘制多个箭头,不显示标记点 + positions = [0.2, 0.5, 0.8] + colors = ['red', 'green', 'blue'] + + for pos, color in zip(positions, colors): + draw_arrow_on_line(ax1, x_start, y_start, x_end, y_end, + position=pos, color=color, show_marker=False) + + ax1.set_xlim(-0.1, 1.1) + ax1.set_ylim(-0.1, 1.1) + ax1.set_aspect('equal') + ax1.grid(True, alpha=0.3) + + # 示例2: 显示标记点 + ax2.set_title("With markers", fontsize=12) + ax2.plot([x_start, x_end], [y_start, y_end], 'gray', linewidth=2, alpha=0.3) + + for pos, color in zip(positions, colors): + draw_arrow_on_line(ax2, x_start, y_start, x_end, y_end, + position=pos, color=color, show_marker=True) + + ax2.set_xlim(-0.1, 1.1) + ax2.set_ylim(-0.1, 1.1) + ax2.set_aspect('equal') + ax2.grid(True, alpha=0.3) + + # 示例3: 使用简洁版本 + ax3.set_title("Using simple version", fontsize=12) + ax3.plot([x_start, x_end], [y_start, y_end], 'gray', linewidth=2, alpha=0.3) + + # 使用简洁版本 + for pos, color in zip(positions, colors): + draw_arrow_on_line_simple(ax3, x_start, y_start, x_end, y_end, + position=pos, color=color) + + ax3.set_xlim(-0.1, 1.1) + ax3.set_ylim(-0.1, 1.1) + ax3.set_aspect('equal') + ax3.grid(True, alpha=0.3) + + plt.tight_layout() + plt.show() + +# 修改你的原始示例 +def your_example(): + """修改后的你的示例""" + fig, ax = plt.subplots(figsize=(8, 8)) + + # 定义线段 + line_x = [0, 1] + line_y = [0, 1] + + # 绘制线段 + ax.plot(line_x, line_y, 'gray', linewidth=3, alpha=0.3, label='Base line') + + # 绘制箭头(不显示标记点) + draw_arrow_on_line(ax, + x_start=line_x[0], y_start=line_y[0], + x_end=line_x[1], y_end=line_y[1], + position=0.5, # 中间位置 + color='blue', + show_marker=False # 不显示标记点 + ) + + # 设置图形属性 + ax.set_xlim(-0.5, 1.5) + ax.set_ylim(-0.5, 1.5) + ax.set_aspect('equal') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + ax.set_title("Arrow without marker point") + + plt.tight_layout() + plt.show() + +# 更多箭头示例 +def multiple_arrows_example(): + """展示多个箭头且不显示标记点的例子""" + fig, ax = plt.subplots(figsize=(10, 8)) + + # 定义多个线段 + lines = [ + ((0, 0), (4, 1), 'Line 1'), + ((1, 0), (3, 3), 'Line 2'), + ((0, 2), (4, 0), 'Line 3'), + ] + + colors = ['blue', 'green', 'red'] + + for i, ((x1, y1), (x2, y2), label) in enumerate(lines): + color = colors[i] + + # 绘制线段 + ax.plot([x1, x2], [y1, y2], color=color, linewidth=2, alpha=0.3, label=label) + + # 在线段上绘制3个箭头(不显示标记点) + for position in [0.25, 0.5, 0.75]: + draw_arrow_on_line(ax, x1, y1, x2, y2, + position=position, + arrow_style='-|>', + color=color, + linewidth=1.5, + head_size=12, + show_marker=False) + + # 设置图形属性 + ax.set_xlim(-0.5, 4.5) + ax.set_ylim(-0.5, 3.5) + ax.set_aspect('equal') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + ax.set_title("Multiple Arrows Without Marker Points") + + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + print("示例1: 你的原始示例(修改版)") + your_example() + + print("\n示例2: 不同选项对比") + example_usage() + + print("\n示例3: 多个箭头示例") + multiple_arrows_example() \ No newline at end of file diff --git a/example/figure/1d/arrow/01h/testprj.py b/example/figure/1d/arrow/01h/testprj.py new file mode 100644 index 00000000..bc6feb33 --- /dev/null +++ b/example/figure/1d/arrow/01h/testprj.py @@ -0,0 +1,335 @@ +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_on_line(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, zorder=2, show_connection=False): + """ + Draw an arrow on a line segment at a specified position. + + Parameters: + ----------- + ax : matplotlib.axes.Axes + The axes to draw on + x_start, y_start : float + Starting point coordinates + x_end, y_end : float + Ending point coordinates + position : float (default=0.5) + Position along the line where to place the arrow (0=start, 1=end) + arrow_style : str (default='->') + Arrow style (e.g., '->', '-|>', '<-', '<->', 'fancy') + color : str or tuple (default='blue') + Arrow color + linewidth : float (default=2) + Arrow line width + head_size : float (default=15) + Arrow head size (mutation_scale parameter) + zorder : int (default=2) + Drawing order (higher values are drawn on top) + show_connection : bool (default=False) + Whether to show the connection line (arrow shaft) + """ + # Calculate the coordinates for the arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction vector + dx = x_end - x_start + dy = y_end - y_start + + # Normalize direction vector + length = np.sqrt(dx**2 + dy**2) + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Define arrow length + arrow_length = 0.2 * length + + # Calculate arrow start and end points + arrow_start_x = arrow_x - 0.4 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.4 * arrow_length * dy_norm + arrow_end_x = arrow_x + 0.4 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.4 * arrow_length * dy_norm + + # Create arrow properties + arrow_props = dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + shrinkA=0, + shrinkB=0) + + # If we don't want the connection line, set linestyle to 'none' + if not show_connection: + arrow_props['linestyle'] = 'none' + + # Draw the arrow + arrow = ax.annotate('', + xy=(arrow_end_x, arrow_end_y), + xytext=(arrow_start_x, arrow_start_y), + arrowprops=arrow_props, + zorder=zorder) + + return arrow + +# 方法2:使用箭头的偏移参数(更直接的方法) +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +# 方法3:使用FancyArrowPatch直接控制 +def draw_arrow_head_only(ax, x_start, y_start, x_end, y_end, position=0.5, + color='blue', linewidth=2, head_size=0.3, zorder=2): + """ + Draw only the arrow head using FancyArrowPatch. + """ + from matplotlib.patches import FancyArrowPatch + + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Arrow length + arrow_length = 0.2 * length + + # Create arrow with specified head properties + arrow = FancyArrowPatch((arrow_x, arrow_y), + (arrow_x + arrow_length * dx_norm, + arrow_y + arrow_length * dy_norm), + arrowstyle='->', + color=color, + linewidth=0, # No line + mutation_scale=head_size * 20, # Scale factor + zorder=zorder) + + ax.add_patch(arrow) + return arrow + +# 示例:对比不同方法 +def compare_methods(): + """对比显示连接线和只显示箭头的方法""" + + fig, axes = plt.subplots(2, 3, figsize=(15, 10)) + axes = axes.flatten() + + # 定义线段 + x_start, y_start = 0, 0 + x_end, y_end = 1, 1 + + methods = [ + ("With connection line", True, draw_arrow_on_line), + ("Without connection line", False, draw_arrow_on_line), + ("draw_arrow_only method", None, draw_arrow_only), + ("draw_arrow_head_only method", None, draw_arrow_head_only), + ("Different arrow styles", None, None), + ("Multiple arrows", None, None), + ] + + for i, (title, show_conn, method_func) in enumerate(methods): + ax = axes[i] + ax.set_xlim(-0.1, 1.1) + ax.set_ylim(-0.1, 1.1) + ax.set_aspect('equal') + ax.grid(True, alpha=0.3) + ax.set_title(title, fontsize=12) + + # 绘制基准线段 + ax.plot([x_start, x_end], [y_start, y_end], + 'gray', linewidth=2, alpha=0.2) + + if i == 0 or i == 1: + # 方法1:使用linestyle控制 + draw_arrow_on_line(ax, x_start, y_start, x_end, y_end, + position=0.5, + color='blue', + show_connection=show_conn, + head_size=20) + + elif i == 2: + # 方法2:draw_arrow_only + draw_arrow_only(ax, x_start, y_start, x_end, y_end, + position=0.5, + color='red', + head_size=20) + + elif i == 3: + # 方法3:draw_arrow_head_only + draw_arrow_head_only(ax, x_start, y_start, x_end, y_end, + position=0.5, + color='green', + head_size=0.4) + + elif i == 4: + # 不同箭头样式 + arrow_styles = ['->', '-|>', '<-', '<->', 'fancy'] + colors = ['red', 'blue', 'green', 'purple', 'orange'] + + for j, (style, color) in enumerate(zip(arrow_styles, colors)): + position = 0.2 + j * 0.15 + draw_arrow_only(ax, x_start, y_start, x_end, y_end, + position=position, + arrow_style=style, + color=color, + head_size=15) + ax.text(position, 0.95, style, ha='center', fontsize=8) + + elif i == 5: + # 多个箭头 + positions = [0.2, 0.4, 0.6, 0.8] + colors = plt.cm.viridis(np.linspace(0, 1, len(positions))) + + for pos, color in zip(positions, colors): + draw_arrow_only(ax, x_start, y_start, x_end, y_end, + position=pos, + color=color, + head_size=15) + + plt.tight_layout() + plt.show() + +# 实用示例:在折线上绘制箭头 +def polyline_with_arrows(): + """在折线上绘制只显示箭头的例子""" + + fig, ax = plt.subplots(figsize=(10, 8)) + + # 定义折线点 + points = [(0, 0), (2, 3), (4, 1), (6, 4), (8, 2)] + x_coords = [p[0] for p in points] + y_coords = [p[1] for p in points] + + # 绘制折线 + ax.plot(x_coords, y_coords, 'gray', linewidth=3, alpha=0.3, + marker='o', markersize=8, label='Polyline') + + # 在每个线段上绘制箭头(只显示箭头头) + for i in range(len(points)-1): + x1, y1 = points[i] + x2, y2 = points[i+1] + + # 在线段的不同位置绘制箭头 + for position in [0.25, 0.5, 0.75]: + draw_arrow_only(ax, x1, y1, x2, y2, + position=position, + arrow_style='-|>', + color=f'C{i}', + linewidth=1.5, + head_size=12) + + # 设置图形属性 + ax.set_xlim(-0.5, 8.5) + ax.set_ylim(-0.5, 4.5) + ax.set_aspect('equal') + ax.grid(True, alpha=0.3) + ax.set_title("Arrow Heads on Polyline (No Connection Lines)") + ax.legend() + + plt.tight_layout() + plt.show() + +# 向量场示例 +def vector_field_example(): + """向量场风格的箭头示例""" + + fig, ax = plt.subplots(figsize=(10, 8)) + + # 创建网格 + x = np.linspace(0, 4, 5) + y = np.linspace(0, 4, 5) + X, Y = np.meshgrid(x, y) + + # 向量场函数(旋转场) + U = -Y + 2 # X分量 + V = X - 2 # Y分量 + + # 绘制向量场(只显示箭头头) + for i in range(len(x)): + for j in range(len(y)): + # 计算向量长度 + length = np.sqrt(U[j, i]**2 + V[j, i]**2) + + if length > 0: + # 归一化 + u_norm = U[j, i] / length + v_norm = V[j, i] / length + + # 绘制箭头(只显示箭头头) + draw_arrow_only(ax, + x_start=X[j, i] - 0.1 * u_norm, + y_start=Y[j, i] - 0.1 * v_norm, + x_end=X[j, i] + 0.1 * u_norm, + y_end=Y[j, i] + 0.1 * v_norm, + position=0.5, + arrow_style='-|>', + color='blue', + head_size=10 + 5 * length) # 长度越大箭头越大 + + # 设置图形属性 + ax.set_xlim(-0.5, 4.5) + ax.set_ylim(-0.5, 4.5) + ax.set_aspect('equal') + ax.grid(True, alpha=0.3) + ax.set_title("Vector Field with Arrow Heads Only") + + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + print("对比不同方法:") + compare_methods() + + print("\n折线箭头示例:") + polyline_with_arrows() + + print("\n向量场示例:") + vector_field_example() \ No newline at end of file diff --git a/example/figure/1d/eno/01/cfd.png b/example/figure/1d/eno/01/cfd.png deleted file mode 100644 index 049e581cdc44987871f70ae654ccd739bdd1c80f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24534 zcmeHPc~n#PwvM$hFBq+3M6`{zaK%iD2y$(>Tf-(d|CXqqrA&_8gC4vyWy$*;# zg(@mSL}VreWrzwG5FtQ-2uK2kBm@WvgoL+40qwnSz5CC5>#et5&RP;ra?Wqq``h3C ze#1GJeze9c|6=182n1sJ-+r+834vIufk1ptUAz#!In9$>hBR8b}G;v zarjj5IUoOEpEItTL)`;|&iMQ3Y46qEt+m-JIQU$Uv5t=K+y&bHfu1^Btmn5uAxqBv za54yi(AHG^n-_l9JQRU&-v2j?eaFIY4|dZ1--gI5gm!1LH*4Je{p6}n^0Jdg+yE;& zO6!u5&aO>HrI&0eH_ts5zUO0q*sH~@v3Ni@|Lr>et#2iZzT>z(S#t4N(N|k{xUVE= zvPCc879fl7CZx9*Q! zA;Jy#xctFd8ndt2OTR}V?A0|lR~JR1Hzrv9`oiDmQcZY=AFx zO-xjgNV^=zOWye#MUm0CmA=RjsWj23b^d4UJcQ!}rM7T9)mCLxe+q>vb

|ttMtyrhRtJ+qcI?MgogB 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 G&;J*~y8g5P diff --git a/example/figure/1d/eno/01c/cfd.png b/example/figure/1d/eno/01c/cfd.png deleted file mode 100644 index 0e2088f70821b64fd98ef03b2b656d49061ba774..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36535 zcmeFacU+TM7cLsRV`H$;1qJLB5u_Ijf{22Z-UJ2dy;pS*6ahsAkrI_bM1%m58X!oE z7$719hzJ1@5CX&ip@oFIUQlPgd(ZE?=l*rix!3%d(S+oE_g;JLwVw5?XUE%T^|Uvx z6Ih2rp*HHA)I5(utrtb1R^r#LhF4BEXRn1nl)O$Dd0lX|_42*!aTRsuvX`5atCy3# z^$wq_9-j8DF0xWeQimjVT=VjB^Hi3WcK-DSDOV3W>79BjC~%QK+)f&MqEJ#|$bZZH z(lvZgs2Fh_&7&9nl1I9_T+O_n^e_lJ|E|{M8H_&Adg{He*{45-`Hy;D$8P5B6dV0i zUz=}saJV>WeMkA*V=BXcb#FfrV}R3LpQ)mC%%Sn=HuuI8q?6Sr?zRr^kQDV{ugQGM zlNG<*eG`3WS*t~Ijg(6NJ%UvGJ{5HA#XF<_jYR{7X0z}_}9Cie?_5=Jo%IV zpJykxh_3kOnf>pZ{Wjxo2SNSbXTKu|3iUfSqJF0s)bHep`V9+yqpRPz5%n7upnk)G z->?A6#ow^tH!MK?h6SkKu;4c=_zepnA^heWf3pR@VF7G}-)zDE*07-R*5lOFnCsWC z^YQUTcHw*%dPWDPH*MT_soV?Iv&jh)01cOI2AUUDy9ybu-r^8V3*bC~utijx#8)g+rIEiFw(QIRijdMr9J z5rtq0h9}O|ynA=@w7x!p(MRa} zQX3^BFVA!H=1p$yCQ&08hepth>@~byB5_E zgmj&@zuqNL;s4=pgRI)xC|-qt+e>fF9on#AgG9(knE=Wur1O;pw59O-?pVtl6BZV( z=*tqM&L@ItnE}DoZEKc3k<>ggGLlcy*492?@(1dJBy_66wl$@y;bs_lHiWU6!5Cq+ z(iujgtX*m8?xl}(j5MXF6pc)XX~$fAvl;c&6dBNgH`+D60f)avM!tRfwl>zz&Ms~5 z9jTljQ;=6M)Lb2wtqpCq4=?uKxh5wNg>H8!XeAXz5JwFSmU?PoxUTmTZlk<&%TV%r zpzC%d=@F)?>DIeZDt?*%5B_=l_;L5S z$)R-yWhHV+jkU4u;|6C(HCiepmoECMcitq_?Zg#S#}k-suFlpI-0%rTo2hGUYwJa` z@_@tL{(~tnFJwKzzKlS@Xe}XCzk{1Z)P-O%n;i%bjREiEl;5067JA{rGlOz>+(-!Zcd z8#mU@%=np=`N>R;P`2o+F)x;J=4jxCg)`-dquq6)G3WAGMjuDr{4QU9dvxpKeZqu2 zL$dM30*6pe<~fLIDiJq6eOf~0q{oG&{T3PdI63()dSQOheezrL)iuY4;W|aprEa}d zbPB24a5-?WkoxoonEuM1^RJ4Pt;+Ah?Qq7uFhbR0uxDr=(;bR+UDf}-W2u$uDf0nw}Y{iyZ7(k&#}~*(IYz)JVvAGNm-ld zUAUs*T|bONPPW9A?{C5&mvk}*AN^(nI}lbC*?L9nim9xznCR#(j}KPX)}q7A00{|+ z8lp7*GkMQc;MBa0=`tJTLdq;X0E>yzAe;1tNn+GDzy-g`etL-lsx-(g~%vo z$CSJl8RyY|imbbI`C>=p6K34}s6u&U2dVq)(XZ;XJgN%Ie4kiCgL%FV6oe@bRh|0r zi7hZ7v{qW)jJrY=9fHX<`6Qazwxylj@%5AC(GlmnZ5V?rCUa`(6GkF~?E!XTIk!}m zlpyN94pc!M@m+g=nbnp9cdRK{!R67-a|IFAj!I#9<920_mb&Oi7TiLzi#}o0x94%b z<18?QRJA~BKR-Y7PKa#CHcv8Q9f(w;jKwf5-WIbfA7$-a>W5GA!rihu4K>xhhcANT zy>=9Y+c_f{pz>1z{zmaDLr2~d+z@z)%RgJZB0Ch7#RaIrE-D+Du*D{>yTi-F!=vHz zeJQX!#Vg|5*b|OLE!X7Y*}m-)F3%fnWd)+O8X6kBE?)&FT%3qw=qg4^S@gMcTWJ9v z)cUHbV-1asC(dm(?{XQyQyUjBkJcpT@AP0TOM7%qcF8JakC1HEV7o!fXx2`BJ-xc` z-*bI;tqN1eW)Fj`z!6#8Z_E$Rju){x6bglppMPi$Ni0YtFv&L9RRrPMhq7_X zCNE{hkx{CKynI{wr6pxSu0<2ZxnZ_|{JLy-JmoKHRw5)l$TwTIY@wO+pgvp$7w`Cc zDGbBFn_F8S9-P`#AX53RO!5-TRM-Eo{NgZ=gu25rlzN}|`h2f-P>`yqC=#y6yqBT6 zb|LO^37iq)9>h5ni$}h;wziBuk#!bHjkb`O>W~dGdeudT5G!Ii@7KR9uG62EUBsg? z<>0h<@vv^N0sf6;&7fWtUGL8G$c)s0{qRR#US6tMJwNIlx9Ex^A`TGq)YXL{hrzys zgeZ-}@4a*N`Uuzdy@`;yGc)(;H_$)y^jMWme$?eYCw#?0f^`7{bBWIm;q|L_v{f9{Q_w{YS|C3 zDu*=U{iJc=3w`j>0YhsapQ(6Xp_IhLp&3^2#4!zCX^Xqz;o-HTue~7jZZuIt;*__^_n$l09GXkhh@_{UGdRe8uPh*!Lh32C!MHcn4P|Bu&8v$ z_MFZO;rexl zvmO;2Pc7c@;u1pp6-?ws{#TLqAqmH-&%7c?soSqF+K)9_)HmKwWXukt2{*aa`M|Jl zs_e-YMo}oMC@*w@^D5yF!6u2y%@e3ySkw^})~daU$Pen%qV;~fzTpi35ptB3Bdg~o z*O#8RH@DMqUPI@F6y2T{tuKv&;%TQvFq8Z^G4VF79fO$*R`D5$WH1p@sjh7a19@vvaYcUnr3?gvgn8LA-ikAMHR2Pjq07~Y ztBmqLsa|=!rf~WHa1jR4&#v%<>HYOCHymu;8*eD&92JR=id^93#>Yl3@Nw%?XPDCp z6&%aHw$KfFsjB=?wsm~lRekH=`rN-;R9`>xx001zk9vc(gh~Hv-V*K{{!_NgjG|$* z_Y5tAy(xp8qSm`bIi{K;=KXe4S~a1ukyv+{##;Prqibo|v2Pn;h6YX50f}qm5B!(0 zlGon9990}N>jCIlg7t~W&7jl24=z+k(_MX`$ej6>7F(LD{B1j2O}TOLYGVK8YMm4d z=JK#HaA<`ub4*$kdSS09N7qAeK}nROtr-;DHvw)(5Ogi$e(>Odxv|*f4dV`p)z4cT zMKKm#!l`y%B-fCMQ21zQ3)$HMZ{ac>&TH{)*oUrVnBx=`$>z?pt5sX?-v7SPujl$Y zo`MsJ;T(ges)|BPP-tI!crlbws`vPAtq}=2IEJHF#7qZ6q<}iS?M)qvjt?b!BSj*3 zcF~K`vCk#4KBJ$=jEd)bza+U<7`OQNj&yKysZE|Gu+(S2KDZkD=+IdCbT@#n5kIOO zI+&+D$K15Vhh8$6+JUv}$A_m&SImf3%#RZfJNsH$?U)+vY-dzpD(3d9FU(IIc2*Fz zUf{5|<2lTBOF{-Zr1XWeMTy5QgTT92@E^>l*0URJ{_d7p&nBr(-ZK6cnkwtp)T4B~ z-t$&>uI0B-VKFB1sv*WDwm7jfHl>GWAqt$jNLIEBUe+~;4hC~2_F%7=cS^+FEzIhyZTZqg0ozv3)(GA+o=WQWVJva^G+sTedE$&RL><<_KiuZ z?B}maHV9gqz9_<8+5V?KM<`)P%cMp6}$9j3c{Ex}7UGkr$17Am;5hA?c1 zm}$v>ySFEKg%dC)3e+GhxO+#Qm~g-V^`=a`3`r@I5`(Lmk7YhXKfFbceu z8aQ@aEl}A~r8i2tB2pSZ?{TJcJ21GxtSRp@dKRXlw10RbKWmy!pF!#@WpzkWhn=y> z;nmGUG9N#GE@L0#rASXI#NH2pG@>!_LD2Tufce(@4fbUIc8)1GLnE!F z_a!-4h}%xZiC2CLHrM#g^Z68r6=SY8k8CDizopJMS`q^3Y~n4ac67^JFr#*wo6#KI z=@B>`LZ2>Cyi%M^BIdB~&^f2~kKrNKr3YF>l0?s$By=V%!YaLpGZ8v}j9o zIfIFhHT9j^qn~=WI)ck}GVb#6$E%D!+6%Y6SItfQg>wKXqCPZ*EQ4Gaueh)Q()erJ zK*Wf&n%i3#3HW&=jVA=NX^}G#f<+>SWMsJUb7YLXwf_fM#E9}-`~%msD}P-CuJgm3 zxizzPfTx~rF@$Jg3*kIu8b__If3T;0asn9@l`~ZYr16KY@T0K+1FVJr1ttDLWJ|9v zB7jvJH*A1G?fJr2U9)Q}b-JRVNx`85|a!0KXm?p&V*hzJy+XuIE2^ zXX*9IQ~xzCjU9tXe6Ea9C&-xUbS3l?6Z)_u^(b?kPi-}qh7jx=FV*SKo=YgQq(3oy z7}Dl<1A9ZXf&gmTz25MNH&3tZe1$V}WD4wTnwmUvR{x?RQdC ze5d?QoZa|;Kd=8h8q{gP{F!SYf2YWSW#4dSRi`RC*syE zYuIx>^zJT(k7p+^R)XI{SNYp6>$n44pp!r3v9DjR+`W5OLcLO0K;RApIOrK>2lzW3 z1=0;ZwjG$eW+(Y9i>CO4Dk_3FU%EUNm3zXk?2Wpx;JHm;v`?1I#S&amwmDZiFCFsHLR^I*rH; zq3ve_%p(xi$;#gQfk52=y~c)y)2$8#uM&tnADrZEu3fvvnCK(K5}*aQ->iASXCizh zT!5QfMpkx&9OGF^R_8KU*=R>E)n2Pru9ans>>POuZdTW0u%zfpWtEkkq z8~BgxTfXfIYRg-`Lr?cCUw;Ut?e=$57=4WhpBI1gW?waZa%)(zb4TD&%kJ)!tHxWS zYKto>VjKzt6ciNd_$pJ4Edn%%T0&8!SE+aNrWrU?aw)~BaGldUJ#-r$-hl?mBlPIG;XQ>^iZ2aiDktT4*cpCQmqN6()psb7hy_OxKF~_cmUW*E}^|x1vk5=3@ z93L!O*FE67Oi^1W&gn0Uk8;6NU5AA>8LqkG>KL39eXo(wF->r6m5#{$tdJyLKODH% zI!|(;yzg1_HBDSsj6v4b)8%iK>xRy*Cln-q&LKOT$Y_}z3eJ~^f~IU*^WN#Vif%(8 zNgFHb-P@Ay&aNHrEebfQMpt5oIke%>GncH1WIgGb@jSyawaKL4hQ*ag48X^G!L<3 zEn8?z`C%JHp$f&Q!$G#; zvq9<_K4M`l`5}`-=b_06h#$IAS$L(5Ycd>^`mSo$_^Eaex1M=)YS-=uI^sH;TZX)- zXI7@d^xZ>4McUij?~B*Ja=}VOWo2cN+4EE1g<*!JU1J8aRLsIHO^HoL{Lxx07g3Z` zuG31lAaJL15G_!|m$%<+2R}68Z0hG9geY^Xx_7vg zpWSPD<6WgJkFt^-t?!MQlxw+Fxv;8iVXxQBXm6U6CaNdSNPf9u^08-b<7-~C%u!8^ zcIzq0n^~Nhy-Z?`q+OV~RyyFYk5cit!Za;n#U z0ciT@9hOgC&_7AxH+WZ-fWt0slDQFuQAwRR)y~1<)N|KKr~l*+i^>-kZNkbp^RAyj zCG!Y!BZ*A!=Dm%{(oCXyS?fZymQ7y(o=RdSKd2Yam)PPq_!_a}*mPDq4Ox#hT~1;; zanUq-VxEB1{T+u;ykf-s=nF^H%uzwa05{{F4L$ReNaU(+0)IL61Z7%IT|ShkN?U-g zk2`y8Osd`Ug}p2C;1Q26>c)As~g5w z-*2Z&yRCMEnhm+A;>Kh0ZWXQ4xWdmFEUyjXI{7kf@pfvT+gd2N-qy_UzS+?ts(j91 zA|;gl8pS=evnmc5V;>#A!1>UJ!C(v%+U*Qh_o)6DW6;v&Sl_XnPyr9Rm$In_oy}%r z;c5@zYW{O2gG!16L!3Hh@*hFAH{PMl?lSLnw4f;RZ6acBb%pU6;<(b%JLfaEj{2}V z&MfDkAp&K*9p9WR;!UjJw?Uz!e%Mx`eT~KWEVBF*<%NAISzPSj=G~V-lo5!A{?Cal z@*NS45GQs=hH+e(-S92lU6C6qYdgIC6iE$zhi_6<~vl>E$NJxb_DYv2jUA_c|MO~<~I@qyRJ}07AJFU4_aP`Pt zu&0QP{~t}UND7k)Bk8xvgKEV9htvI+X!5oKx71Wl69|aR zw(NYnYYVhg7p8&g-j4et`8MP?vICP+SHxnm$o;{MFV&J7zYGuWUl(3=j`QeLdJ0K> zQ%0TE%CK0Kf$`1X)&Fx+!6#e1vVQmorrY;WqGtTnlAag`*iExq)R7l=j;1*(wH3sJ+Gn=nnSuXAO z1&M4ZpZ@fX0=bTm4?!w|#lwn|`#nmWYF9MfgPY(k|7-1r)OIF$gJq<<_1~?Y(g*MK zXW=PME@qaNp+*TW?19?~dg<(T(xG?0mBu9ZzqRTq+F$`biCO@>!lLFSRq}uHQkE9J z{E5aiNI>UoSU&g`R;=sajxYYh@%>pE5UAWAU!@E?9ngjCCR#NW)O5#0QM%FAlR3L- z^$I)B8-{Bk(A)>LmHNC8IHSt7KN1Gg6N~Jy6o-E31p}#}XcZM^p3A+=$~UH@tQA1b z*}X!OR5F>2-MHZ9@_I1WmB43%rAXxDJ=CFXY1*20yn2r&Ys7QR9`t% z-?gBBe2lh6R!gF8xKP$N^r80#pA(YF`jTF_F7n&MfMz(yXKq`@cA;ioL&G^s`^K1V zp3z-bYQ^m3uhzSC^7B?W@{-nc8c`p^I$C^wyDm8=Ly+5P?V1iXqOMy=t0b$CF5?p% zyxp=Q=r+Y^Du*B~GTl9Tyzsy>iDUBMH$>)RKo|B;a&o2v%A?sP5gRD4i|Wqyq>N8~el}A~4-ZX%ns}5yIJFyJgoy=2Dw6GrW!;2;O~}{`BzSD(1j<>A z@MAmU#BW3FuQpf~B%JVP{&fur{?OO8>;5TL_tjpE+6%chpQbycZMPN+hS)&rX+~nW=HG$ z0JH$`H_VV%c@q&4k%Hf&C^7AqmXzEd{FtS3?LS;2&CP_wQ{S zNrZIZ)4Wh%;%pNb0wHlJkmP#1DLVANVNc^1q9Z#37A|Ynz#<{rgJP4_6NGI1b8QMP zceT>8MT%5+UDVVpF>5;f;1Iffnoc!Luzd1%2sl!YzciBh5L*!c$>|9Yy%uwj4;k=M zABDQ|v(g@iUazjtpCK!hAoKUwoqpavbmL7~QAnsKj4i)eXzZ#t?f&$~YH9vW5m`j( z#^-8$8@^0UP2D@QJfsR@ST**aQ2JMecR$r4}hPRNR z#*|O$?+$&8ltB)0Dl?&YVcarQt}bL}C>$W3JZ`j=NjzO>@_lTY#hqt#GSt3rtt#Xb zgM_HsFW7B&DNa1o)6*rk1FNS7(*(JN{$73${ZAT~qUIczM@kcOh|vgwfPl@-t@6k4 zwy4^d23cA4FaPR9rYq-->-q+m`d??>1+=`x1y4@=}I{Kpc01q+z2f zyfDe>a#4WVDKZj*+V-d6W;eiD$o&cO@5wYKMD+-#k0%0@v9s6Y0^I8)X86JAG1cQA z$BgBzsSw?4?~cY(SM_ZD5mQa@RLfN#S?mz#nsRB_;kqoMv&Fp>a=+65iEWney#;Yzv4d>EFtKA8X%BLf{el5t$McR zmGtFquR?h8ZyP0F9!}et6d4J{9e}oxwh#>##}A3%M0UQev?NZ7>(u-^4EbQRgc7?wN6R{isi%H>yb+=mjim{<7uD``7JYI`)7pU_nl{$iZKr%!g_T zas%8M!uFds_dXBy%l} zrK;DM5VooSm7oIxDCA)ky*&+us+9Wi^3Y;PTKo<2KShnjEwuPN<8PDf0cCGBcsI#t z=^~j2(WF6w%gSHxN4Z%bW&c0V!zRT3O$vV9{rlOHaeiCkr<43IZHaju2<8AE&h6cb zlth5sV7J*#6Z0kDuV&`vC!U>+gc`TYWqPgTE%w><-(?)u^0Y3p;%+%k)W1SA*BG5$ zpZk=@O6A5o^y&DkKouaN4yhd(44IJm8Ey!=`yZWx07VYo^FU{QP-EdXq+_+ZKKF*f z2?+nd-8?^^Da0@bIJ9bp@9@g?4vX4bMZ-eZDtzk-iI=P37WZ0`_4^nM_f+xvvUZvp z&KaVgn6)tX1f-kLprB;lsM=YmAd4-t$R=I%A!j$K*5yHW1#UnrKpx2lIu&{?hnK28b6i;#4Y!z_`JkfFXYLH-<3eHkf8m0P{Aiz6qEy zQi3RI&ENp`AJz^d4PnVurGU1pdk?;78Olql;#VmUIFzFlf8pgSq6#vV%E08RsYcTK zkm!~);TUZ-gwHdYnn;l3Zsz2I2?7Yv~ys6$OVA&W4kUE?ueZK10pS~ zX=8X1kiw;AS>)7h?w}Lxb>jn?)jcp!knG?s(#Xz*u|isr;oBhJeH@5U5I)0&VNrW^ zhquf*75tg!N3wrX2l?mp?k#<(TuP6Sp%(t1hgt_iO&8vzua>;~*HDd!)W77;SeBKy z4826a()=HvySh#km_$QG9e`&_*Fy?W$EoSWJPHWb}^WHbpK%@)Ww zqb3v|plI&U>ozi|;tYTWJ_U{lH;C=p>VcFMRtvYPFN0s8yY~?D$rl&6-3zs?0bfT( zl3SHi-NQ_D*ePnQjwbZ=*}!7*@&@g|TzI*&@5u$X5tH1q#XM2go>$}= zB0uGrEYEZ^8t(PV46n{Z)8D`!f{>npOYdX&dHc%={IL_gQ&u!|dAS9>QM#PDuL-Dv zPW#4$3;FoM4*@{0a`zI4AIzODK(U_Qvg++9u<~C#AK{0wai#us;kmHYQAwc~c2LF{ zNg?e#Be?8=415R#s&0G*Z-(H0oa5#6E8eLuL{J zASLHof{$_)2gx_2wQ3BIB|tjh)?j1= zKh+p;WEk^pA6)Rr!2O#$4hd@i34E(q;gzC*a*NX$EdaETH$!Kvoj9TRl zlMXJ{Y*VaBM>gDfhS8@jClDQtR}hGXsle=^1`^Kn{<@rgDI9%XF9?|(J)b~*kK#2J>8Vt^>cf^t|uFvIyKp$hiKHe9Zci6OY%~Ydx9-mGe6gpYob>gp@hUWlK z17$hc;V=MVm3+p4WoNl00?weKvbD&X@Q9B2HZgjWCnHI=7 zESI+K*CHnvz=&_p+m-x;WUxNh5$>w@?AMlce>a+B|3nMQ!_m$nG8JNeTP(WKHY=BB zq}nbg;#7J*cVV9=+$Vb#YgRA8q{CSf!Zmo5X%{ecBa;ObuA^V|UD(nSPdxrv~}HcaZd6{kh1lr zq8w&xAsSk+anlb`lf%0St?XvP@MvH;oz7r5e+NZ94|P0$5_WaH6J+?64h|n0)=hD@YDD z%IQCo!&moHoeF}{b!E-hR9P$!l~B!wjFzFO*x;a`paSQNcaS*$77mdT9RTo=K~h3z zMO^rsC&a}P@wK6I>otlnCXY$HXx9Q0b24%}7!cODtq+=Aydyurl9?3fL>R-aG}+2shm}35z{<9B|}fwDI)b->CrP5P%cqg4t3y zBv<;GKCPfA(C@* zgJ*+tIe!L#2gHj!bTYa-*RgzFG0*PZY7wN?!CQb@?{MAIR_o_C&05jBpJZaXTrg75 zI#I&~A4+^Y6dU#Prbv^@lN-7R5uCT_XV;sF_6hui#A86FL1r5ucRmm|Zv2t}J&HDb zWTe~~aA2;K@%cZUh*wRAkE+2$TbHkQI4YSed-RG=&c*$~GrE04c)2sN?7|%iOOO@5 z3fMT_`S1lQY(UjbzFZ6E5g4xMixjkG zsnBpo&}LIAy@(nTC2d(TKdC-38)S!BgL}?nrEu2~Vl-mvN+lc~Pjz@v;ZcID9PpsP z5drpul<~KHDR%|s7TY`d)$#Uf7(`pa2k3EQSn<>lmgM{X@vNe(HQjKvqmpi8>paUB zuLqYLIR{7)&_z3~+@C6U#xP^IG(bqGMDb znUDG4N`%*E=pktsfJbG?7R>=s$=J>N9-V?R=lR9`Cjhh5UxDV?+oPGQr0%5?AM4r0K=G3MJ(Gt>g=QAdgT+7(n=uk?^5& z*#_Wi+31Y3do|<4Wm5E}$*twNk_p~}7olsu6L@vt%~I4d0<3f-U#>!CRV-(#Iq)3V zLqpdCJW*NFFVJ0FQrAs-HuJat+>aL&0BBN}bGxgyHL+>4;b5Zj=@ElmUEMc*Na<&t=l8j; zlKVgWVsU7huq8kYv}x|i@O22n$oQhGg4EF>IBqvYg95Z1u;WO3Hb{qI|9|=X`i1jc zU{3g0@}8qt)We~r2Pz7fT=TW9I&nfc9k;Cx*LAO$Ro^gK^l8Q5!v24%-$5F}TTMC7 zuDX>KAmaV$_ zC+w|_5!C=Ikgg}P(k`H%$BFHk+4|p>N&B)oniSi$wj&+7vk}JU#-FFYfB(}s z9yTb~akOp*PqAj*VW`&;i-fh;+!=)96&vv~HX@1pH+tD+$m3 z%SZeX>dwj#N(=+bv)qi4z3Ym_|BKBN>Y=~$>v6=z_TPseJ)p(>>vyQ%&wkqCwg284@L(>{Gi=a9=8Fz(=vA}2- zm;^JG0EvJc>M+!5%S=P~AklN6ou93q1S$pifIG-tT81V&8leCvJ_$8^Md(6Bo^YX= zC6t*^?!|N=qQRbq#R?&)^2#A#{ z(fDXWUwOp zYE8VCFzTlqH35e2w%h`rtOm(!7LjE^M=CsZuEn`Zf)o~MB16kS6PTqBp1XB6r|1o) zonhqyhz4mK)T9wrWcWaG4%D3qjf=h5k@rYN0JF=4Z&Lxg8@4|Xzip6J@L<;)Pm=*d?z>y<8NU#v_>Nsj+;~8i@!j8BEy1<$%hbYDR@mGO0 zhb^N8n+$16Jv!y??Y#}8x90TAFsVf~>}Eql!=(Zr(P&{4)1~1k0;1S|bm}Uzo60gC zFYV_$uyq~sNg!e=GciC|1(a9F@Zh?}xJ`ndK+gwgl}o|kSFqKEMMTnowv+st9_l-I zk@c9ZSj#aWl@a#E^sntpoB%H0Afdh85gjv{(6${30^K0_0mb&6j9yAGnllUSPtZkX z6>&j9<%4UcPm7&tfr<*4PWiDG`ZO{bm^v_Kh;DCji#mPwg}K1GKurWY8jdaaly)P_ z4krVkmWBpdZ9B#i$}ABiPRI!$-xdRL?a`@za}pjDihWR`pGyCzGb%*NciQ%~0*{Xc z;sGKD3`aj%(YJVI|vsMqE$m(y$R^PAUOf&&%l$a z2IfFF#2?e%iL`w`l}bV<4LHt0Aa6lW(;lLOX-@(IHo9Fm?n+iWG>QeCC3vDti`>y| z)CEVhX|_QUoYU~nA_uhFHCQL_))}|nsF%?~9t4(T5E+kko+7At1*c6H=F#0gL*Nk# z-9XVg7JLd)6&AtSPt2Mvj8$b$%iz4i#4S*w0-@iilE6X_U3Cz|Gro4jkZ{*x{(r^yf3TB-WB2^u%7M#-OiGB zH;DO8haI~}dI??E!v4A&JzH-_>0ld?-%d_jl`@r79^-+}L~!TC1RczflRjW3VjbU-%j~$cTjjInL0BM$Z2jwy(MQ)*#lg zz=Xao0XDspnSR{YjAw(v5VNzU7tSA?&&*gBKfMxPWZwb~1@v*+{DDWO0{@68>Kp~! zki}2|Nu;8X`ucE^Kgj#{Ng|haWPaKa51Itqj8JlnlLRp`ajx z8zG(u`4c*gaD)T8A_D=b$CWeG2l|@Q@%qnFH4fnQadxUABpJd)x|DvbR$4R-v}7|w zbJX#t^%6w+s!5nE3NyQeYI^z*g(WyS0gDGJ9=&>RH;tsRU^Xfa41LKBG1$k`uirtY z0>TavJ;i+`QzE$1Eijnw1=F2hBWv#l=(5(Gv$V7%!11XVp64V%TWZ~*`R`s@BN=Vf ziIyWFHP#J4w;-|spc2iFeuu<+rllB%gEL(o=XYjiEUHIS$b-<#?t?^S<5PFRV;wut z({l@0fA!6`braBf0k#HyuVDK`qtKIPqdt&4$J#nPi)FChXw*&gro8v7sOcYyY~;V6 zu;!IZXAHCn5|}zL&z^h`Pohv|R+cK)r~BvT=BzUEQX6drWy1$HZo?aco}t?+(y`J}O|I z;lZnTbR4jnpdtmVwO`0G+n{J&qEKfZtLT>sU{xx(5oq9$V@Ghk)|(k=R$|Z`tsEh{3JKDLX(AjzgF`BJaxfS)dlbHgj7(egTI7GS`|w(x|_s z=RU-Y12a-rCA9fSX7Nn#`ye$6&LYgifyOLin>h?E%y}ME4DgraSCIR`a(i4X;4sX< z#OyHDOF38um9VI!hDC;+MNtUH+0KOXlG!q;6_m5RMW3}WFkqWSD7WJ|R9Mt4-8ogS zBnam`j$8qK3cwdCU8Lj^%kwS=&ZHq&lS+u-q~hMFhYW>1G8d}xa3Qgt=+fxuStn{c zy3tO6$XzEp*jsLAYmDZeJxfbOOy|F3y4sfQ1;+8CBXR|RCm}^m2{hVHm}8}c_wR+a z(ou`St!gP0I-FDJ7+;yn;2?$5K1!(p?UEN!|nNeAcO1Sf3W?@yeInYYfnrp@`oi2h)D zHUra-U(3RN96K9LpfK`Vj0_Ux?R6b5Puav`XH0Mn2-+t!7rZ2xP@*lZ2~AW9-g2W)t} zkhW8ww0vSx;wX$9CA|n}XKsTO+1X5FQ-rody}e(6LpRiwaM1L7+wpcpCq68**gL4T zcE@0|Y>j!4gNpkf#f;PWuxc36dA9NKZdetl%m9BN${{GVsC3NSSNq3blZCQE;h(ew z%oAzU@Gz+1phr#rqPo^^!Wt^0sJd-fBFHs_(`$mZSzI0 zRKRHz!9YVfY@*e3&b+ZrF z9(q59m*EHqH1OOo8TJ}iiHTte&pwmn#KvJooR2SeU3CfS0*XWj5;HCWdQXnAW(=?P;)^=3+o5z2FvPJ_F3re?$!bn0k|5zUW(96 zXxcR;gTcMwsB5Z8bIsJ}Bl2h{ghd~q(8L3E7C=9z`H%^j1bK)VgStNU z#1NXMiFyT90KQ=mh0aO?1 ztl@70v~Fe|B2d>+r*sP=Chyz52R4+=?g#b5<1PI6p@|EJp+th?$({wm8_vXiZ6xKj z3?+BnjS@jxK5_y`ArFvnZfrm7UzZ~NIhGxjo%D~2WkarTccNZ~smAoq6i<-{)RL*` zop?;bvUZP>GUdls{_;^p5()Me$~TwcymM@nzD&dn)?dlbGb8#NS=v!-*;m=9&v5Wr z4Ivjgo-FQf9$*xpTS#VtC?2?iYEt0)qfzaE-=fb}%U_Q4rl%|`hNKJ-Dxf^%A<4P3 zg5HnxALpgnkjW8N{e2ae3X!dnW+($=gg+x8k^dR`1=r1T%siGx-pB+=q^kD&1g zg$V@9*v%5)T`%kfz~*3;ELj{Tl!ORjU`jH{)*@I9fIx(tL2ux3nvUdhgdp{ss`(^Y z?ZbIGEJmsP*Q7djn{o%B*v5bq@!OPpkT1i`;LH*VRoFV@CGYc2KBJoAY*mE4y zpZO_)&3@Aa+euMv#`Ne@*bY!Mv_!byskH_w3EA5g7+#&Y*UJroC7C|DitY^_r@Hp3 zK~{OdkxJyG2_{quYmYu9;>nlsftqoaM8g!QuW-sXFfH$v$CgOhq5Hq=?gu73DQ#AC>_%JBVz9Lo@`j0C^D0+#R6jZ4j2cd2}!bzM&Kx6@ono zOtw{k)I%k4lx+ui8@^cpxG?g*v39(EJk=he3uKRJ-^sk~*jS}98|L)B)`^e0vXwc~ z=rZq~*0~YvU=6)xjE_X{weOqlfS>}77a3_tLUHHunG)StAjnZe^0<{F{JX{|k0iZ} z35NpZ*hn&7D@_Kim!T2-*-teUPDuM}HmCsdr)L{Df?gN_A~sN#m9-2tm}a+?!C~++ zAI%1!sgPITjrwRi3a399T@f4k7(PuN)sW@eU=Zt1JB+-?B)ciFych^R~p&sAq^W-3|raYzn&}bCMoZ@siT->j0V@s7UpJCN=lg z*+d}-QJ>AR9uAi@Uuysj5f(3v$B2C)_rHOpWZ3C*XI|Z$LvP899?YOg;U*L7CS*Xt ziq6ph2`zaPj%QS6BYq?mzPQ)dK)q>qv>P-`00ZFG2A8`**!AhYg;c^|A7h5%c^D%3 z*gOEmtdttp(bu80;uhwSQSDSab<2^@7OP#rICX(Wt5Xx6-DkFk3{hJ?5OnEq5R+E} zf86-i#Wt3^sxS!|=Jpxl_66>|Qr9fN>hg4T0aQzRN&%4r0dU|0=*cv+Sa)6$r*W3Fg(=BUyr}5z^QZfQT&iCuc)xpjGqY=*;v%k z;DLM^IfynqJlx1{1*sHhyvYBfy(^1pA_&7P8e#y!6Cn~0K%)t%ROB$hdQde;kOw05 zYCzG{7>+_CZEC9(A5c+DdoZ@KAy-X%*1C4kn`Uh7H0ND3CduGeE8 zP{j4CAizS)7CiG*VO9d%j3hUW*B?mq0|5SUE(a)cO~Nv?I>mS?bI&nbmD3GJ|Z* zv_;5M2q5}qPHbSf5+BusK@bt=4CB=N5~~yOdah06Ta^zC`x+Z|H&7a|E7tuPty9imbKo5c(xQnE=)Hx6#NQRj_nvOpZS=wA8l|*M5#0V= z2qmf;CKZ+kv+Vhh*wn54a2rH~&x6nI!-g0XEyD~ua8A$_gYpx+ag8s6^6_~@p{ z*IsWj`$9%NFVC_z>!J||lR>7~qh%3_!;KPedCo3mi{CZVN%;T1yAx(=IJR%;RhoGQ zo@`jlKK`BXX0qsd(^Ub-9G69X%$xs9)3O#s zDuHcbEZc(Y9>XRW+z!7t0R)c(j|33ld&h(s@JR4T@cm)J26!ZRBnSfk&ntdTHy1>Z zDnzz8)PA3e!|*p_7R{i^QAD)KKPt*&@FlTtn_bLi*z^E@5r+#8#6JPTt5LPS!#Hom PL++NiVT-tSeMaF|zv_lG diff --git a/example/figure/1d/eno/01e/cfd.png b/example/figure/1d/eno/01e/cfd.png deleted file mode 100644 index 486ca80383db7d748e5f02247f0afcbfef632928..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36158 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 IIVCg!0A`Y-_5c6? diff --git a/example/figure/1d/eno/01f/cfd.png b/example/figure/1d/eno/01f/cfd.png deleted file mode 100644 index 11ab81bd5c024aad4e2a54001a0f11e61cac861f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36295 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 diff --git a/example/figure/1d/eno/02h/cfd.png b/example/figure/1d/eno/02h/cfd.png deleted file mode 100644 index aaea00d6400089f4ee3ed4385b43b47043a60a6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37308 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 0 and i < im: + continue + if i > ip and i < nx-1: + continue + xcc_new.append( xcc[i] ) + plt.scatter(xcc_new, np.full_like(xcc_new, yref), s=20, facecolor='black', edgecolor='black', linewidth=1) + return + +def plot_cell_center_rs( xcc, yref, r, s ): + nx = xcc.size + ii = nx // 2 + xcc_new = [] + for m in range(-r, s+1): + xcc_new.append( xcc[ii+m] ) + plt.scatter(xcc_new, np.full_like(xcc_new, yref), s=100, facecolor='black', edgecolor='black', linewidth=1) + return + +def plot_mesh( x, yref ): + dx = x[1] - x[0] + dy = 0.1 * dx + nx = x.size + for i in range(0, nx): + xm = x[i] + plt.plot([xm, xm], [yref-dy, yref+dy], 'k-') # 绘制垂直线 + + nxc = x.size - 1 + + for i in range(0, nxc): + plt.plot([x[i], x[i+1]], [yref, yref], 'b-', linewidth=1) + return + +def plot_mesh_rs( x, yref, r, s): + dx = x[1] - x[0] + dy = 0.1 * dx + + nxc = xcc.size + ii = nxc // 2 + + idc = [] + + for m in range(-r, s+1): + #print(f'm={m}, r={r}, s={s}') + idc.append( ii+m ) + #print(f"idc={idc}") + idv = idc.copy() + idv.append(idc[-1]+1) + + ncell = len( idc ) + nvertex = len( idv ) + for i in range(0, nvertex): + xm = x[ idv[i] ] + plt.plot([xm, xm], [yref-dy, yref+dy], 'k-') # 绘制垂直线 + + for i in range(0, ncell): + plt.plot([x[idc[i]], x[idc[i]+1]], [yref, yref], 'b-', linewidth=1) + return + +def plot_label(x, xcc, yref): + dx = x[1] - x[0] + dyb = 0.5 * dx + dyt = dyb * 0.6 + yb = yref - dyb + yt = yref + dyt + ybc = yref - 0.5* dyb + plt.text(x[0], yb, r'$x_{i-\frac{5}{2}}$', fontsize=12, ha='center') + plt.text(x[1], yb, r'$x_{i-\frac{3}{2}}$', fontsize=12, ha='center') + plt.text(x[2], yb, r'$x_{i-\frac{1}{2}}$', fontsize=12, ha='center') + plt.text(x[3], yb, r'$x_{i+\frac{1}{2}}$', fontsize=12, ha='center') + plt.text(x[4], yb, r'$x_{i+\frac{3}{2}}$', fontsize=12, ha='center') + plt.text(x[5], yb, r'$x_{i+\frac{5}{2}}$', fontsize=12, ha='center') + + nx = xcc.size + i = nx // 2 + print("i=",i) + im = i - 1 + im1 = i - 2 + ip = i + 1 + ip1 = i + 2 + + plt.text(xcc[im1], ybc, r'$i-2$', fontsize=12, ha='center') + plt.text(xcc[im], ybc, r'$i-1$', fontsize=12, ha='center') + plt.text(xcc[i], ybc, r'$i$', fontsize=12, ha='center') + plt.text(xcc[ip], ybc, r'$i+1$', fontsize=12, ha='center') + plt.text(xcc[ip1], ybc, r'$i+2$', fontsize=12, ha='center') + return + +def plot_label_rs(x, xcc, yref, r, s): + dx = x[1] - x[0] + dyb = 0.5 * dx + dyt = dyb * 0.6 + yb = yref - dyb + yt = yref + dyt + ybc = yref - 0.5* dyb + ytt = yref + 0.5* dyt + namelist = [] + namelist.append(r'$x_{i-\frac{5}{2}}$') + namelist.append(r'$x_{i-\frac{3}{2}}$') + namelist.append(r'$x_{i-\frac{1}{2}}$') + namelist.append(r'$x_{i+\frac{1}{2}}$') + namelist.append(r'$x_{i+\frac{3}{2}}$') + namelist.append(r'$x_{i+\frac{5}{2}}$') + + nx = xcc.size + ii = nx // 2 + + idc = [] + for m in range(-r, s+1): + idc.append( ii+m ) + idv = idc.copy() + idv.append(idc[-1]+1) + + ncell = len( idc ) + nvertex = len( idv ) + + for i in range(0, nvertex): + xm = x[ idv[i] ] + name = namelist[ idv[i] ] + #plt.text(xm, yb, name, fontsize=12, ha='center') + plt.text(xm, ytt, name, fontsize=12, ha='center') + + for m in range(-r, s+1): + ss = '-' + if m > 0 : + ss = '+' + str = r'$i' + ss + f'{abs(m)}' + r'$' + if m == 0 : + plt.text(xcc[ii+m], ybc, r'$i$', fontsize=12, ha='center') + else: + plt.text(xcc[ii+m], ybc, str, fontsize=12, ha='center') + + str = r'$' + f'({r=},{s=})' + r'$' + ishift = (-r+s)//2 + plt.text(xcc[ii+ishift], yb, str, fontsize=12, ha='center') + + return + +def getrs(k,rv,sv): + kk = k-1 + for m in range(0, k): + s = m + r = kk - s + rv.append( r ) + sv.append( s ) + return + +# 设置字体为 Times New Roman +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +# 设置图形大小和样式 +plt.figure(figsize=(12, 6)) + +nx = 5 +L = 1.0 +x_l = 0.0 +dx = L / nx + +x = np.zeros(nx+1, dtype=np.float64) +xcc = np.zeros(nx, dtype=np.float64) + +for i in range(0, nx+1): + x[i] = x_l + dx*(i) + +for i in range(0, nx): + xcc[i] = 0.5*(x[i]+x[i+1]) + +print("x=",x) +print("xcc=",xcc) + +k=3 +rv = [] +sv = [] +getrs(k,rv,sv) +print(f'{rv=},{sv=}') + +dyref = 0.2 + +size = len(rv) +print(f'{size=}') +for i in range(0, size): + yref = 0.0 - i * dyref + r=rv[i] + s=sv[i] + plot_cell_center_rs( xcc, yref, r, s) + plot_mesh_rs( x, yref, r, s) + plot_label_rs(x, xcc, yref, r, s) + + +plt.axis('equal') +plt.axis('off') + +plt.savefig('cfd.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/eno/03a/testprj.py b/example/figure/1d/eno/03a/testprj.py new file mode 100644 index 00000000..7d2138c0 --- /dev/null +++ b/example/figure/1d/eno/03a/testprj.py @@ -0,0 +1,218 @@ +import numpy as np +import matplotlib.pyplot as plt + +def plot_all_cell_center( xcc, yref ): + plt.scatter(xcc, np.full_like(xcc, yref), s=20, facecolor='black', edgecolor='black', linewidth=1) + return + +def plot_cell_center( xcc, yref ): + nx = xcc.size + ii = nx // 2 + im = ii - 1 + ip = ii + 1 + xcc_new = [] + for i in range(0, nx): + if i > 0 and i < im: + continue + if i > ip and i < nx-1: + continue + xcc_new.append( xcc[i] ) + plt.scatter(xcc_new, np.full_like(xcc_new, yref), s=20, facecolor='black', edgecolor='black', linewidth=1) + return + +def plot_cell_center_rs( xcc, yref, r, s ): + nx = xcc.size + ii = 2 + xcc_new = [] + for m in range(-r, s+1): + print(f"m={m},r={r},s={s},ii={ii},ii+m={ii+m}") + xcc_new.append( xcc[ii+m] ) + plt.scatter(xcc_new, np.full_like(xcc_new, yref), s=100, facecolor='black', edgecolor='black', linewidth=1) + return + +def plot_mesh( x, yref ): + dx = x[1] - x[0] + dy = 0.1 * dx + nx = x.size + for i in range(0, nx): + xm = x[i] + plt.plot([xm, xm], [yref-dy, yref+dy], 'k-') # 绘制垂直线 + + nxc = x.size - 1 + + for i in range(0, nxc): + plt.plot([x[i], x[i+1]], [yref, yref], 'b-', linewidth=1) + return + +def plot_mesh_rs( x, yref, r, s): + dx = x[1] - x[0] + dy = 0.1 * dx + + nxc = xcc.size + ii = 2 + + idc = [] + + for m in range(-r, s+1): + #print(f'm={m}, r={r}, s={s}') + print(f"m={m},r={r},s={s},ii={ii},ii+m={ii+m}") + idc.append( ii+m ) + #print(f"idc={idc}") + idv = idc.copy() + idv.append(idc[-1]+1) + + ncell = len( idc ) + nvertex = len( idv ) + for i in range(0, nvertex): + xm = x[ idv[i] ] + plt.plot([xm, xm], [yref-dy, yref+dy], 'k-') # 绘制垂直线 + + for i in range(0, ncell): + plt.plot([x[idc[i]], x[idc[i]+1]], [yref, yref], 'b-', linewidth=1) + return + +def plot_label(x, xcc, yref): + dx = x[1] - x[0] + dyb = 0.5 * dx + dyt = dyb * 0.6 + yb = yref - dyb + 0.1 + yt = yref + dyt + ybc = yref - 0.5* dyb + plt.text(x[0], yb, r'$x_{i-\frac{5}{2}}$', fontsize=12, ha='center') + plt.text(x[1], yb, r'$x_{i-\frac{3}{2}}$', fontsize=12, ha='center') + plt.text(x[2], yb, r'$x_{i-\frac{1}{2}}$', fontsize=12, ha='center') + plt.text(x[3], yb, r'$x_{i+\frac{1}{2}}$', fontsize=12, ha='center') + plt.text(x[4], yb, r'$x_{i+\frac{3}{2}}$', fontsize=12, ha='center') + plt.text(x[5], yb, r'$x_{i+\frac{5}{2}}$', fontsize=12, ha='center') + plt.text(x[6], yb, r'$x_{i+\frac{7}{2}}$', fontsize=12, ha='center') + + nx = xcc.size + i = nx // 2 + print("i=",i) + im = i - 1 + im1 = i - 2 + ip = i + 1 + ip1 = i + 2 + ip2 = i + 3 + + plt.text(xcc[im1], ybc, r'$i-2$', fontsize=12, ha='center') + plt.text(xcc[im], ybc, r'$i-1$', fontsize=12, ha='center') + plt.text(xcc[i], ybc, r'$i$', fontsize=12, ha='center') + plt.text(xcc[ip], ybc, r'$i+1$', fontsize=12, ha='center') + plt.text(xcc[ip1], ybc, r'$i+2$', fontsize=12, ha='center') + plt.text(xcc[ip2], ybc, r'$i+3$', fontsize=12, ha='center') + return + +def plot_label_rs(x, xcc, yref, r, s, namelist): + dx = x[1] - x[0] + dyb = 0.5 * dx + dyt = dyb * 0.6 + yb = yref - dyb + yt = yref + dyt + ybc = yref - 0.5* dyb + ytt = yref + 0.5* dyt + + nx = xcc.size + ii = 2 + + idc = [] + for m in range(-r, s+1): + print(f"m={m},r={r},s={s},ii={ii},ii+m={ii+m}") + idc.append( ii+m ) + idv = idc.copy() + idv.append(idc[-1]+1) + + ncell = len( idc ) + nvertex = len( idv ) + + for i in range(0, nvertex): + xm = x[ idv[i] ] + name = namelist[ idv[i] ] + #plt.text(xm, yb, name, fontsize=12, ha='center') + plt.text(xm, ytt, name, fontsize=12, ha='center') + + for m in range(-r, s+1): + ss = '-' + if m > 0 : + ss = '+' + str = r'$i' + ss + f'{abs(m)}' + r'$' + if m == 0 : + plt.text(xcc[ii+m], ybc, r'$i$', fontsize=12, ha='center') + else: + plt.text(xcc[ii+m], ybc, str, fontsize=12, ha='center') + + str = r'$' + f'({r=},{s=})' + r'$' + ishift = (-r+s)//2 + plt.text(xcc[ii+ishift], yb, str, fontsize=12, ha='center') + + return + +def getrs(k,rv,sv): + kk = k-1 + for m in range(0, k+1): + s = m + r = kk - s + rv.append( r ) + sv.append( s ) + return + +# 设置字体为 Times New Roman +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +# 设置图形大小和样式 +plt.figure(figsize=(12, 6)) + +nx = 5 + 1 +L = 1.0 +x_l = 0.0 +dx = L / nx + +x = np.zeros(nx+1, dtype=np.float64) +xcc = np.zeros(nx, dtype=np.float64) + +for i in range(0, nx+1): + x[i] = x_l + dx*(i) + +for i in range(0, nx): + xcc[i] = 0.5*(x[i]+x[i+1]) + +print("x=",x) +print("xcc=",xcc) + +k=3 +rv = [] +sv = [] +getrs(k,rv,sv) +print(f'{rv=},{sv=}') + +namelist = [] +namelist.append(r'$x_{i-\frac{5}{2}}$') +namelist.append(r'$x_{i-\frac{3}{2}}$') +namelist.append(r'$x_{i-\frac{1}{2}}$') +namelist.append(r'$x_{i+\frac{1}{2}}$') +namelist.append(r'$x_{i+\frac{3}{2}}$') +namelist.append(r'$x_{i+\frac{5}{2}}$') +namelist.append(r'$x_{i+\frac{7}{2}}$') + +dyref = 0.2 +size = len(rv) +print(f'{size=}') + + +for i in range(0, size): + yref = 0.0 - i * dyref + r=rv[i] + s=sv[i] + plot_cell_center_rs( xcc, yref, r, s) + print(f"plot_mesh_rs ,r={r},s={s}") + plot_mesh_rs( x, yref, r, s) + print(f"plot_label_rs ,r={r},s={s}") + plot_label_rs(x, xcc, yref, r, s, namelist) + + +plt.axis('equal') +plt.axis('off') + +plt.savefig('cfd.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/eno/03b/testprj.py b/example/figure/1d/eno/03b/testprj.py new file mode 100644 index 00000000..5dd2fa38 --- /dev/null +++ b/example/figure/1d/eno/03b/testprj.py @@ -0,0 +1,100 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只需调这几个) ========================== +visual_cell_width = 2.0 # 调大一点让整体占满(2.0 在 12 英寸宽下几乎完美左右贴边) +center_x = 6.0 # 6.0 基本居中(因为左右延伸不对称 2.5 vs 3.5) +dyref = 1.5 # 行间垂直间距 +k = 3 # 保持 3 → 产生 r = 2,1,0,-1 四行 +# ========================================================================= + +def plot_cell_center_rs(yref, r, s): + ms = list(range(-r, s + 1)) # 自动支持 r 负(Python range 能处理) + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=120, facecolor='black', edgecolor='black') + +def plot_mesh_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + dy = 0.12 * visual_cell_width + + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + + # 垂直黑线 + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.5) + + # 水平蓝线 + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.4) + +def plot_label_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + dyb = 0.5 * visual_cell_width + yb = yref - dyb - 0.1 # (r,s) 稍微低一点 + ybc = yref - 0.5 * dyb + ytt = yref + 0.52 * dyb # vertex 标签在最上方 + + # === vertex 标签(只画当前行实际存在的) === + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + offset = v + frac = int(round(abs(offset) * 2)) # 1,3,5,7 + sign = '+' if offset > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + offset * visual_cell_width + plt.text(xv, ytt, label, fontsize=12, ha='center', va='bottom') + + # === cell 标签 === + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, ybc, r'$i$', fontsize=14, ha='center', color='red') + elif m > 0: + plt.text(xc, ybc, rf'$i+{m}$', fontsize=12, ha='center') + else: + plt.text(xc, ybc, rf'$i-{ -m }$', fontsize=12, ha='center') + + # (r,s) 标注 + shift = (-r + s) / 2.0 + xc = center_x + shift * visual_cell_width + plt.text(xc, yb, rf'$(r={r},\; s={s})$', fontsize=13, ha='center', color='darkblue') + +def getrs(k, rv, sv): + kk = k - 1 + for m in range(0, k + 1): + s_val = m + r_val = kk - s_val + rv.append(r_val) + sv.append(s_val) + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(12, 6.5)) # 稍微高一点容纳 4 行 + +rv, sv = [], [] +getrs(k, rv, sv) +print(f'生成的 (r,s) 顺序: {list(zip(rv, sv))}') # [ (2,0), (1,1), (0,2), (-1,3) ] + +for i in range(len(rv)): + yref = -i * dyref + r = rv[i] + s = sv[i] + plot_cell_center_rs(yref, r, s) + plot_mesh_rs(yref, r, s) + plot_label_rs(yref, r, s) + +plt.axis('off') +plt.xlim(-1, 13) # 基本贴边(2.0 时 -5→+7 相对单位) +plt.ylim(-len(rv)*dyref - 1, 1.5) +plt.tight_layout() +plt.savefig('cfd_stencil_bias.png', bbox_inches='tight', dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/eno/03c/testprj.py b/example/figure/1d/eno/03c/testprj.py new file mode 100644 index 00000000..28f2160f --- /dev/null +++ b/example/figure/1d/eno/03c/testprj.py @@ -0,0 +1,97 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只需调这几个) ========================== +visual_cell_width = 2.0 # 2.0 时左右几乎完美贴边(12英寸宽) +center_x = 5.0 # 精确居中(左2.5 + 右3.5 = 6个单元 → center=2.5×2.0=5.0) +dyref = 2.35 # 行间距,彻底消除垂直交叉(可继续调大更疏朗) +k = 3 # 保持3 → 产生你想要的4种偏置 +# ========================================================================= + +def plot_cell_center_rs(yref, r, s): + ms = list(range(-r, s + 1)) + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140, facecolor='black', edgecolor='black', linewidth=1) + +def plot_mesh_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + dy = 0.12 * visual_cell_width + + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + + # 垂直黑线 + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.6) + + # 水平蓝线(cell) + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.6) + +def plot_label_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + dyb = 0.5 * visual_cell_width # 1.0 + y_vertex = yref + 0.68 * dyb # vertex 标签(最上方) + y_cell = yref - 0.68 * dyb # cell 标签(中间偏下) + y_rs = yref - 1.05 * dyb # (r,s) 标签(最下方,保证不交叉) + + # === vertex 标签(只画当前行实际存在的)=== + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=13, ha='center', va='bottom') + + # === cell 标签 === + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=15, ha='center', color='red', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=13, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=13, ha='center') + + # === (r,s) 标注(放在最下方,永远不会和下一行 vertex 交叉)=== + shift = (-r + s) / 2.0 + xc = center_x + shift * visual_cell_width + plt.text(xc, y_rs, rf'$(r={r},\;s={s})$', fontsize=14, ha='center', color='darkblue', weight='bold') + +def getrs(k, rv, sv): + kk = k - 1 + for m in range(0, k + 1): + s_val = m + r_val = kk - s_val + rv.append(r_val) + sv.append(s_val) + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(12, 7.8)) # 稍微高一点,4行更舒适 + +rv, sv = [], [] +getrs(k, rv, sv) +print(f'生成的 (r,s) 顺序: {list(zip(rv, sv))}') # 你会看到 [(2, 0), (1, 1), (0, 2), (-1, 3)] + +for i in range(len(rv)): + yref = -i * dyref + r = rv[i] + s = sv[i] + plot_cell_center_rs(yref, r, s) + plot_mesh_rs(yref, r, s) + plot_label_rs(yref, r, s) + +plt.axis('off') +plt.tight_layout() +plt.savefig('cfd_stencil_final.png', bbox_inches='tight', dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/eno/03d/testprj.py b/example/figure/1d/eno/03d/testprj.py new file mode 100644 index 00000000..e3abb5e4 --- /dev/null +++ b/example/figure/1d/eno/03d/testprj.py @@ -0,0 +1,95 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数 ========================== +visual_cell_width = 2.0 +center_x = 5.0 # 精确居中(左2.5 + 右3.5 = 6个单元宽) +dyref = 2.35 # 行间距(保证上下完全不交叉) +k = 3 +# ========================================================================= + +def plot_cell_center_rs(yref, r, s): + ms = list(range(-r, s + 1)) + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140, facecolor='black', edgecolor='black', linewidth=1) + +def plot_mesh_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + dy = 0.12 * visual_cell_width + + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.6) + + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.6) + +def plot_label_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + dyb = 0.5 * visual_cell_width # = 1.0 + # 关键调整:vertex 标签大幅贴近网格线 + y_vertex = yref + 0.28 * dyb # ← 原来0.68,现在0.28,明显更近 + y_cell = yref - 0.68 * dyb + y_rs = yref - 1.05 * dyb # (r,s) 仍保持在最下方,安全距离 + + # === vertex 标签(只画当前行实际存在的)=== + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=13.5, ha='center', va='bottom', color='black') + + # === cell 标签 === + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=15, ha='center', color='red', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=13, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=13, ha='center') + + # === (r,s) === + shift = (-r + s) / 2.0 + xc = center_x + shift * visual_cell_width + plt.text(xc, y_rs, rf'$(r={r},\;s={s})$', fontsize=14, ha='center', color='darkblue', weight='bold') + +def getrs(k, rv, sv): + kk = k - 1 + for m in range(0, k + 1): + s_val = m + r_val = kk - s_val + rv.append(r_val) + sv.append(s_val) + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(12, 7.8)) + +rv, sv = [], [] +getrs(k, rv, sv) + +for i in range(len(rv)): + yref = -i * dyref + r = rv[i] + s = sv[i] + plot_cell_center_rs(yref, r, s) + plot_mesh_rs(yref, r, s) + plot_label_rs(yref, r, s) + +plt.axis('off') +plt.tight_layout() +plt.savefig('cfd_stencil_final_tighter.png', bbox_inches='tight', dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/eno/03e/testprj.py b/example/figure/1d/eno/03e/testprj.py new file mode 100644 index 00000000..5e570484 --- /dev/null +++ b/example/figure/1d/eno/03e/testprj.py @@ -0,0 +1,116 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # x方向宽度(英寸),改大 → 更宽 +fig_height = 8.0 # y方向高度(英寸),改小 → 更扁 +# 长宽比随意调,例如: +# fig_width = 20.0, fig_height = 7.0 → 约 2.85:1 +# fig_width = 18.0, fig_height = 6.0 → 3:1 +# fig_width = 14.0, fig_height = 9.0 → 约 1.55:1 + +k = 3 # 你的偏置阶数 +# ========================================================================= + +# === 缩放因子(以 16×8 为基准)=== +base_width = 16.0 +base_height = 8.0 +scale_x = fig_width / base_width +scale_y = fig_height / base_height + +# === 水平尺寸(永远占满宽度)=== +visual_cell_width = fig_width / 6.0 # 最大跨度正好 6 个 cell +center_x = 2.5 * visual_cell_width # 精确居中(左2.5 + 右3.5) + +# === 垂直尺寸(完全独立于水平)=== +base_dyref = 1.85 # 基准高度 8 英寸、4 行时的行间距 +rv, sv = [], [] +kk = k - 1 +for m in range(0, k + 1): + s_val = m + r_val = kk - s_val + rv.append(r_val) + sv.append(s_val) +num_rows = len(rv) + +dyref = base_dyref * scale_y * (4.0 / max(num_rows, 1)) # 行数自动适配 +vertical_half = dyref * 0.54 # ≈1.0(基准时),所有垂直偏移都基于它 + +def plot_cell_center_rs(yref, r, s): + ms = list(range(-r, s + 1)) + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + +def plot_mesh_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + dy = 0.25 * vertical_half # 短竖线长度随垂直缩放 + + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.8*scale_x) + + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.8*scale_x) + +def plot_label_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + y_vertex = yref + 0.25 * vertical_half + y_cell = yref - 0.68 * vertical_half + y_rs = yref - 1.18 * vertical_half + + # vertex 标签 + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) + + + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=14*scale_y, ha='center', va='bottom') + + # cell 标签 + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=16*scale_y, ha='center', + color='red', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=14*scale_y, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=14*scale_y, ha='center') + + # (r,s) + shift = (-r + s) / 2.0 + xc = center_x + shift * visual_cell_width + plt.text(xc, y_rs, rf'$(r={r},\;s={s})$', fontsize=15*scale_y, ha='center', + color='darkblue', weight='bold') + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(fig_width, fig_height)) + +for i in range(num_rows): + yref = -i * dyref + r = rv[i] + s = sv[i] + plot_cell_center_rs(yref, r, s) + plot_mesh_rs(yref, r, s) + plot_label_rs(yref, r, s) + +plt.axis('off') +plt.tight_layout(pad=0.3) +plt.savefig('cfd_stencil_perfect_ratio.png', bbox_inches='tight', dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/eno/03f/testprj.py b/example/figure/1d/eno/03f/testprj.py new file mode 100644 index 00000000..4d4ad9ae --- /dev/null +++ b/example/figure/1d/eno/03f/testprj.py @@ -0,0 +1,120 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # x方向宽度(英寸) +fig_height = 8.0 # y方向高度(英寸),随意改实现任意长宽比 +# 示例:20x8、18x6、24x9 等都完美自适应不交叉 + +k = 3 +# ========================================================================= + +# === 缩放因子(以 16×8 为基准)=== +base_width = 16.0 +base_height = 8.0 +scale_x = fig_width / base_width +scale_y = fig_height / base_height + +# === 水平尺寸(图形略微放大 → 边距自然极小)=== +visual_cell_width = fig_width / 6.9 # 6.9 → 边距更接近 Word “窄”边距(≈0.4~0.6cm) +center_x = 3.0 * visual_cell_width # 完美居中(左2.5 + 右3.5 的平均) + +# === 垂直尺寸 === +base_dyref = 1.85 +rv, sv = [], [] +kk = k - 1 +for m in range(0, k + 1): + s_val = m + r_val = kk - s_val + rv.append(r_val) + sv.append(s_val) +num_rows = len(rv) + +dyref = base_dyref * scale_y * (4.0 / max(num_rows, 1)) +vertical_unit = dyref * 0.54 + +def plot_cell_center_rs(yref, r, s): + ms = list(range(-r, s + 1)) + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + +def plot_mesh_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + dy = 0.25 * vertical_unit + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.8*scale_x) + + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.8*scale_x) + +def plot_label_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + y_vertex = yref + 0.25 * vertical_unit + y_cell = yref - 0.68 * vertical_unit + y_rs = yref - 1.05 * vertical_unit + + # vertex 标签 + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=14*scale_y, ha='center', va='bottom') + + # cell 标签 + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=16*scale_y, ha='center', + color='red', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=14*scale_y, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=14*scale_y, ha='center') + + # (r,s) + shift = (-r + s) / 2.0 + xc = center_x + shift * visual_cell_width + plt.text(xc, y_rs, rf'$(r={r},\;s={s})$', fontsize=15*scale_y, ha='center', + color='darkblue', weight='bold') + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(fig_width, fig_height)) + +for i in range(num_rows): + yref = -i * dyref + r = rv[i] + s = sv[i] + plot_cell_center_rs(yref, r, s) + plot_mesh_rs(yref, r, s) + plot_label_rs(yref, r, s) + +# === 极小边距(真正像 Word “窄”边距)=== +margin_x = 0.12 * visual_cell_width # 左右 ≈0.3~0.5cm +margin_y = 0.25 * vertical_unit # 上下极小(保证不裁剪最后一行的 (r,s)) + +min_x = center_x - 2.5 * visual_cell_width - margin_x +max_x = center_x + 3.5 * visual_cell_width + margin_x +min_y = -(num_rows-1)*dyref - 1.3*vertical_unit - margin_y +max_y = 0 + 0.4*vertical_unit + margin_y + +plt.xlim(min_x, max_x) +plt.ylim(min_y, max_y) +plt.axis('off') + +plt.savefig('cfd_stencil_word_narrow.png', bbox_inches='tight', pad_inches=0.02, dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/finite_difference/01/testprj.py b/example/figure/1d/finite_difference/01/testprj.py new file mode 100644 index 00000000..98f0600f --- /dev/null +++ b/example/figure/1d/finite_difference/01/testprj.py @@ -0,0 +1,500 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + #self.cellbased = 1 + self.cellbased = 0 + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot_cell_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_node_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + lw = 2 # Line width + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + icellmax = self.ncells - 1 + cindex = 0 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + for i in range(len(left_list)): + isrc, itgt, color = left_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[itgt], self.ghost_mesh_left.ycc[itgt] + + dy = i * vlen + end_y_src = src_y + vlen + offset_y + dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + for i in range(len(right_list)): + isrc, itgt, color = right_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[itgt], self.ghost_mesh_right.ycc[itgt] + + dy = i * vlen + #end_y_src = src_y + vlen + offset_y + dy + end_y_src = src_y - vlen - offset_y - dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,0, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/finite_difference/01a/testprj.py b/example/figure/1d/finite_difference/01a/testprj.py new file mode 100644 index 00000000..f569dd3e --- /dev/null +++ b/example/figure/1d/finite_difference/01a/testprj.py @@ -0,0 +1,614 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + + def plot_vertical_lines_at_cell_center(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + for i in range(self.ncells): + xm = self.xcc[i] + ym = self.ycc[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + #self.cellbased = 1 + self.cellbased = 0 + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_node_label(self): + ytext_shift = 0.8*abs(self.dx) + for i in range(self.nnodes): + if self.lr == "L": + node_label = f"${-i+1+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + node_label = f"$N$" + else: + node_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.x[i], self.y[i]-ytext_shift, node_label, fontsize=12, ha='center') + + def plot_cell_based_figure(self): + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + def plot_node_based_figure(self): + self.plot_node_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.x, self.y, s=50, facecolor='red', edgecolor='black', linewidth=1) + + self.plot_vertical_lines_at_cell_center() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + #self.cellbased = 1 + self.cellbased = 0 + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_node_label(self): + dytext = 0.8*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.nnodes): + if i < self.nlabels: + node_label = f"${i+1+self.ishift}$" + elif i > self.nnodes - 1 - self.nlabels: + inew = i - (self.nnodes - 1) + self.ishift + 1 + node_label = self.get_dollar_label("N",inew) + else: + node_label="" + # Add text label at cell center + plt.text(self.x[i], self.y[i]-dytext, node_label, fontsize=12, ha='center') + icenter = self.nnodes // 2 + plt.text(self.x[icenter], self.y[icenter]-dytext, f"$i$", fontsize=12, ha='center') + node_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + #xm = self.xcc[self.ncells-1] + self.dx + #ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={node_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_node(self): + # Plot main mesh cell-center points (black fill with black edge) + x_new = [] + y_new = [] + for i in range(self.nnodes): + if self.nodemark[i] == 1: + x_new.append( self.x[i] ) + y_new.append( self.y[i] ) + plt.scatter(x_new, y_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot_node_mesh(self): + self.icenter = self.nnodes // 2 + self.nlabels = self.nghosts + self.nodemark = np.zeros(self.nnodes, dtype=int) + for i in range(self.nnodes): + if i < self.nlabels: + self.nodemark[i] = 1 + elif i > self.nnodes - 1 - self.nlabels: + self.nodemark[i] = 1 + self.nodemark[self.icenter] = 1 + + self.plot_node() + self.plot_node_label() + + # Plot horizontal line connecting main mesh nodes + for ic in range(self.ncells): + ls_left = "k-" if self.nodemark[ic] == 1 else "k--" + plt.plot([self.x[ic], self.xcc[ic]], [self.y[ic], self.ycc[ic]], ls_left, linewidth=1) + + ls_right = "k-" if self.nodemark[ic+1] == 1 else "k--" + plt.plot([self.xcc[ic], self.x[ic+1]], [self.ycc[ic], self.y[ic+1]], ls_right, linewidth=1) + + def plot_cell_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_node_based_figure(self): + self.plot_node_mesh() + self.plot_vertical_lines_at_cell_center() + self.plot_boundary_vertical_interface_lines() + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + lw = 2 # Line width + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + icellmax = self.ncells - 1 + cindex = 0 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + for i in range(len(left_list)): + isrc, itgt, color = left_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[itgt], self.ghost_mesh_left.ycc[itgt] + + dy = i * vlen + end_y_src = src_y + vlen + offset_y + dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + for i in range(len(right_list)): + isrc, itgt, color = right_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[itgt], self.ghost_mesh_right.ycc[itgt] + + dy = i * vlen + #end_y_src = src_y + vlen + offset_y + dy + end_y_src = src_y - vlen - offset_y - dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,0, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/finite_difference/01b/testprj.py b/example/figure/1d/finite_difference/01b/testprj.py new file mode 100644 index 00000000..87cefef2 --- /dev/null +++ b/example/figure/1d/finite_difference/01b/testprj.py @@ -0,0 +1,600 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + + def plot_vertical_lines_at_cell_center(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + for i in range(self.ncells): + xm = self.xcc[i] + ym = self.ycc[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + #self.cellbased = 1 + self.cellbased = 0 + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_node_label(self): + ytext_shift = 0.8*abs(self.dx) + for i in range(self.nnodes): + if self.lr == "L": + node_label = f"${-i+1+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + node_label = f"$N$" + else: + node_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.x[i], self.y[i]-ytext_shift, node_label, fontsize=12, ha='center') + + def plot_cell_based_figure(self): + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + def plot_node_based_figure(self): + self.plot_node_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.x, self.y, s=50, facecolor='red', edgecolor='black', linewidth=1) + + self.plot_vertical_lines_at_cell_center() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + #self.cellbased = 1 + self.cellbased = 0 + nghostcells = self.nghosts + #if self.cellbased == 0: + # nghostcells = self.nghosts - 1 + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_node_label(self): + dytext = 0.8*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.nnodes): + if i < self.nlabels: + node_label = f"${i+1+self.ishift}$" + elif i > self.nnodes - 1 - self.nlabels: + inew = i - (self.nnodes - 1) + self.ishift + 1 + node_label = self.get_dollar_label("N",inew) + else: + node_label="" + # Add text label at cell center + plt.text(self.x[i], self.y[i]-dytext, node_label, fontsize=12, ha='center') + icenter = self.nnodes // 2 + plt.text(self.x[icenter], self.y[icenter]-dytext, f"$i$", fontsize=12, ha='center') + node_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + #xm = self.xcc[self.ncells-1] + self.dx + #ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={node_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_node(self): + # Plot main mesh cell-center points (black fill with black edge) + x_new = [] + y_new = [] + for i in range(self.nnodes): + if self.nodemark[i] == 1: + x_new.append( self.x[i] ) + y_new.append( self.y[i] ) + plt.scatter(x_new, y_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot_node_mesh(self): + self.icenter = self.nnodes // 2 + self.nlabels = self.nghosts + self.nodemark = np.zeros(self.nnodes, dtype=int) + for i in range(self.nnodes): + if i < self.nlabels: + self.nodemark[i] = 1 + elif i > self.nnodes - 1 - self.nlabels: + self.nodemark[i] = 1 + self.nodemark[self.icenter] = 1 + + self.plot_node() + self.plot_node_label() + + # Plot horizontal line connecting main mesh nodes + for ic in range(self.ncells): + ls_left = "k-" if self.nodemark[ic] == 1 else "k--" + plt.plot([self.x[ic], self.xcc[ic]], [self.y[ic], self.ycc[ic]], ls_left, linewidth=1) + + ls_right = "k-" if self.nodemark[ic+1] == 1 else "k--" + plt.plot([self.xcc[ic], self.x[ic+1]], [self.ycc[ic], self.y[ic+1]], ls_right, linewidth=1) + + def plot_cell_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_node_based_figure(self): + self.plot_node_mesh() + self.plot_vertical_lines_at_cell_center() + self.plot_boundary_vertical_interface_lines() + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + + def draw_periodic_connections_wrap(self, coor_list, ghost_mesh, coeff=1): + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + for i in range(len(coor_list)): + isrc, itgt, color = coor_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = ghost_mesh.xcc[itgt], ghost_mesh.ycc[itgt] + + dy = i * vlen + end_y_src = src_y + coeff *( vlen + offset_y + dy ) + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + icellmax = self.ncells - 1 + cindex = 0 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + self.draw_periodic_connections_wrap(left_list, self.ghost_mesh_left) + self.draw_periodic_connections_wrap(right_list, self.ghost_mesh_right,-1) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,0, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/finite_difference/01c/testprj.py b/example/figure/1d/finite_difference/01c/testprj.py new file mode 100644 index 00000000..2ed1761d --- /dev/null +++ b/example/figure/1d/finite_difference/01c/testprj.py @@ -0,0 +1,646 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + + def plot_vertical_lines_at_cell_center(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + for i in range(self.ncells): + xm = self.xcc[i] + ym = self.ycc[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + #self.cellbased = 1 + self.cellbased = 0 + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_node_label(self): + ytext_shift = 0.8*abs(self.dx) + for i in range(self.nnodes): + if self.lr == "L": + node_label = f"${-i+1+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + node_label = f"$N$" + else: + node_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.x[i], self.y[i]-ytext_shift, node_label, fontsize=12, ha='center') + + def plot_cell_based_figure(self): + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + def plot_node_based_figure(self): + self.plot_node_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.x, self.y, s=50, facecolor='red', edgecolor='black', linewidth=1) + + self.plot_vertical_lines_at_cell_center() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + #self.cellbased = 1 + self.cellbased = 0 + nghostcells = self.nghosts + #if self.cellbased == 0: + # nghostcells = self.nghosts - 1 + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_node_label(self): + dytext = 0.8*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.nnodes): + if i < self.nlabels: + node_label = f"${i+1+self.ishift}$" + elif i > self.nnodes - 1 - self.nlabels: + inew = i - (self.nnodes - 1) + self.ishift + 1 + node_label = self.get_dollar_label("N",inew) + else: + node_label="" + # Add text label at cell center + plt.text(self.x[i], self.y[i]-dytext, node_label, fontsize=12, ha='center') + icenter = self.nnodes // 2 + plt.text(self.x[icenter], self.y[icenter]-dytext, f"$i$", fontsize=12, ha='center') + node_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + #xm = self.xcc[self.ncells-1] + self.dx + #ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={node_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_node(self): + # Plot main mesh cell-center points (black fill with black edge) + x_new = [] + y_new = [] + for i in range(self.nnodes): + if self.nodemark[i] == 1: + x_new.append( self.x[i] ) + y_new.append( self.y[i] ) + plt.scatter(x_new, y_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot_node_mesh(self): + self.icenter = self.nnodes // 2 + self.nlabels = self.nghosts + self.nodemark = np.zeros(self.nnodes, dtype=int) + for i in range(self.nnodes): + if i < self.nlabels: + self.nodemark[i] = 1 + elif i > self.nnodes - 1 - self.nlabels: + self.nodemark[i] = 1 + self.nodemark[self.icenter] = 1 + + self.plot_node() + self.plot_node_label() + + # Plot horizontal line connecting main mesh nodes + for ic in range(self.ncells): + ls_left = "k-" if self.nodemark[ic] == 1 else "k--" + plt.plot([self.x[ic], self.xcc[ic]], [self.y[ic], self.ycc[ic]], ls_left, linewidth=1) + + ls_right = "k-" if self.nodemark[ic+1] == 1 else "k--" + plt.plot([self.xcc[ic], self.x[ic+1]], [self.ycc[ic], self.y[ic+1]], ls_right, linewidth=1) + + def plot_cell_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_node_based_figure(self): + self.plot_node_mesh() + self.plot_vertical_lines_at_cell_center() + self.plot_boundary_vertical_interface_lines() + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + + def draw_periodic_connections_wrapOld(self, coor_list, ghost_mesh, coeff=1): + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + for i in range(len(coor_list)): + isrc, itgt, color = coor_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = ghost_mesh.xcc[itgt], ghost_mesh.ycc[itgt] + + dy = i * vlen + end_y_src = src_y + coeff *( vlen + offset_y + dy ) + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + def draw_periodic_connections_wrap(self, coor_list, xsrc, ysrc, xtgt, ytgt, coeff=1): + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + for i in range(len(coor_list)): + isrc, itgt, color = coor_list[i] + src_x, src_y = xsrc[isrc], ysrc[isrc] + tgt_x, tgt_y = xtgt[itgt], ytgt[itgt] + + dy = i * vlen + end_y_src = src_y + coeff *( vlen + offset_y + dy ) + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + if self.cellbased == 1: + icellmax = self.ncells - 1 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + else: + icellmax = self.ncells - 1 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + self.draw_periodic_connections_wrap( + left_list, + self.xcc, + self.ycc, + self.ghost_mesh_left.xcc, + self.ghost_mesh_left.ycc + ) + self.draw_periodic_connections_wrap( + right_list, + self.xcc, + self.ycc, + self.ghost_mesh_right.xcc, + self.ghost_mesh_right.ycc, + -1 + ) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/finite_difference/01d/testprj.py b/example/figure/1d/finite_difference/01d/testprj.py new file mode 100644 index 00000000..995ff708 --- /dev/null +++ b/example/figure/1d/finite_difference/01d/testprj.py @@ -0,0 +1,636 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + + def plot_vertical_lines_at_cell_center(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + for i in range(self.ncells): + xm = self.xcc[i] + ym = self.ycc[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + #self.cellbased = 1 + self.cellbased = 0 + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_node_label(self): + ytext_shift = 0.8*abs(self.dx) + for i in range(self.nnodes): + if self.lr == "L": + node_label = f"${-i+1+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + node_label = f"$N$" + else: + node_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.x[i], self.y[i]-ytext_shift, node_label, fontsize=12, ha='center') + + def plot_cell_based_figure(self): + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + def plot_node_based_figure(self): + self.plot_node_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.x, self.y, s=50, facecolor='red', edgecolor='black', linewidth=1) + + self.plot_vertical_lines_at_cell_center() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + #self.cellbased = 1 + self.cellbased = 0 + nghostcells = self.nghosts + #if self.cellbased == 0: + # nghostcells = self.nghosts - 1 + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_node_label(self): + dytext = 0.8*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.nnodes): + if i < self.nlabels: + node_label = f"${i+1+self.ishift}$" + elif i > self.nnodes - 1 - self.nlabels: + inew = i - (self.nnodes - 1) + self.ishift + 1 + node_label = self.get_dollar_label("N",inew) + else: + node_label="" + # Add text label at cell center + plt.text(self.x[i], self.y[i]-dytext, node_label, fontsize=12, ha='center') + icenter = self.nnodes // 2 + plt.text(self.x[icenter], self.y[icenter]-dytext, f"$i$", fontsize=12, ha='center') + node_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + #xm = self.xcc[self.ncells-1] + self.dx + #ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={node_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_node(self): + # Plot main mesh cell-center points (black fill with black edge) + x_new = [] + y_new = [] + for i in range(self.nnodes): + if self.nodemark[i] == 1: + x_new.append( self.x[i] ) + y_new.append( self.y[i] ) + plt.scatter(x_new, y_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot_node_mesh(self): + self.icenter = self.nnodes // 2 + self.nlabels = self.nghosts + self.nodemark = np.zeros(self.nnodes, dtype=int) + for i in range(self.nnodes): + if i < self.nlabels: + self.nodemark[i] = 1 + elif i > self.nnodes - 1 - self.nlabels: + self.nodemark[i] = 1 + self.nodemark[self.icenter] = 1 + + self.plot_node() + self.plot_node_label() + + # Plot horizontal line connecting main mesh nodes + for ic in range(self.ncells): + ls_left = "k-" if self.nodemark[ic] == 1 else "k--" + plt.plot([self.x[ic], self.xcc[ic]], [self.y[ic], self.ycc[ic]], ls_left, linewidth=1) + + ls_right = "k-" if self.nodemark[ic+1] == 1 else "k--" + plt.plot([self.xcc[ic], self.x[ic+1]], [self.ycc[ic], self.y[ic+1]], ls_right, linewidth=1) + + def plot_cell_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_node_based_figure(self): + self.plot_node_mesh() + self.plot_vertical_lines_at_cell_center() + self.plot_boundary_vertical_interface_lines() + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + + def draw_periodic_connections_wrap(self, coor_list, xsrc, ysrc, xtgt, ytgt, coeff=1): + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + for i in range(len(coor_list)): + isrc, itgt, color = coor_list[i] + src_x, src_y = xsrc[isrc], ysrc[isrc] + tgt_x, tgt_y = xtgt[itgt], ytgt[itgt] + + dy = i * vlen + end_y_src = src_y + coeff *( vlen + offset_y + dy ) + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + if self.cellbased == 1: + icellmax = self.ncells - 1 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + self.draw_periodic_connections_wrap( + left_list, + self.xcc, + self.ycc, + self.ghost_mesh_left.xcc, + self.ghost_mesh_left.ycc + ) + self.draw_periodic_connections_wrap( + right_list, + self.xcc, + self.ycc, + self.ghost_mesh_right.xcc, + self.ghost_mesh_right.ycc, + -1 + ) + else: + inodemax = self.nnodes - 1 + for i in range(self.nghosts): + left_list.append((inodemax-i, i+1, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i+1, next(color_cycle))) + + self.draw_periodic_connections_wrap( + left_list, + self.x, + self.y, + self.ghost_mesh_left.x, + self.ghost_mesh_left.y + ) + self.draw_periodic_connections_wrap( + right_list, + self.x, + self.y, + self.ghost_mesh_right.x, + self.ghost_mesh_right.y, + -1 + ) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/finite_difference/01e/testprj.py b/example/figure/1d/finite_difference/01e/testprj.py new file mode 100644 index 00000000..2613bd5a --- /dev/null +++ b/example/figure/1d/finite_difference/01e/testprj.py @@ -0,0 +1,636 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + + def plot_vertical_lines_at_cell_center(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + for i in range(self.ncells): + xm = self.xcc[i] + ym = self.ycc[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + #self.cellbased = 1 + self.cellbased = 0 + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_node_label(self): + ytext_shift = 0.8*abs(self.dx) + for i in range(self.nnodes): + if self.lr == "L": + node_label = f"${-i+1+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + node_label = f"$N$" + else: + node_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.x[i], self.y[i]-ytext_shift, node_label, fontsize=12, ha='center') + + def plot_cell_based_figure(self): + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + def plot_node_based_figure(self): + self.plot_node_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.x, self.y, s=50, facecolor='red', edgecolor='black', linewidth=1) + + self.plot_vertical_lines_at_cell_center() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + #self.cellbased = 1 + self.cellbased = 0 + nghostcells = self.nghosts + if self.cellbased == 0: + nghostcells = self.nghosts - 1 + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_node_label(self): + dytext = 0.8*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.nnodes): + if i < self.nlabels: + node_label = f"${i+1+self.ishift}$" + elif i > self.nnodes - 1 - self.nlabels: + inew = i - (self.nnodes - 1) + self.ishift + 1 + node_label = self.get_dollar_label("N",inew) + else: + node_label="" + # Add text label at cell center + plt.text(self.x[i], self.y[i]-dytext, node_label, fontsize=12, ha='center') + icenter = self.nnodes // 2 + plt.text(self.x[icenter], self.y[icenter]-dytext, f"$i$", fontsize=12, ha='center') + node_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + #xm = self.xcc[self.ncells-1] + self.dx + #ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={node_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_node(self): + # Plot main mesh cell-center points (black fill with black edge) + x_new = [] + y_new = [] + for i in range(self.nnodes): + if self.nodemark[i] == 1: + x_new.append( self.x[i] ) + y_new.append( self.y[i] ) + plt.scatter(x_new, y_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot_node_mesh(self): + self.icenter = self.nnodes // 2 + self.nlabels = self.nghosts + self.nodemark = np.zeros(self.nnodes, dtype=int) + for i in range(self.nnodes): + if i < self.nlabels: + self.nodemark[i] = 1 + elif i > self.nnodes - 1 - self.nlabels: + self.nodemark[i] = 1 + self.nodemark[self.icenter] = 1 + + self.plot_node() + self.plot_node_label() + + # Plot horizontal line connecting main mesh nodes + for ic in range(self.ncells): + ls_left = "k-" if self.nodemark[ic] == 1 else "k--" + plt.plot([self.x[ic], self.xcc[ic]], [self.y[ic], self.ycc[ic]], ls_left, linewidth=1) + + ls_right = "k-" if self.nodemark[ic+1] == 1 else "k--" + plt.plot([self.xcc[ic], self.x[ic+1]], [self.ycc[ic], self.y[ic+1]], ls_right, linewidth=1) + + def plot_cell_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_node_based_figure(self): + self.plot_node_mesh() + self.plot_vertical_lines_at_cell_center() + self.plot_boundary_vertical_interface_lines() + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + + def draw_periodic_connections_wrap(self, coor_list, xsrc, ysrc, xtgt, ytgt, coeff=1): + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + for i in range(len(coor_list)): + isrc, itgt, color = coor_list[i] + src_x, src_y = xsrc[isrc], ysrc[isrc] + tgt_x, tgt_y = xtgt[itgt], ytgt[itgt] + + dy = i * vlen + end_y_src = src_y + coeff *( vlen + offset_y + dy ) + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + if self.cellbased == 1: + icellmax = self.ncells - 1 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + self.draw_periodic_connections_wrap( + left_list, + self.xcc, + self.ycc, + self.ghost_mesh_left.xcc, + self.ghost_mesh_left.ycc + ) + self.draw_periodic_connections_wrap( + right_list, + self.xcc, + self.ycc, + self.ghost_mesh_right.xcc, + self.ghost_mesh_right.ycc, + -1 + ) + else: + inodemax = self.nnodes - 1 + for i in range(self.nghosts): + left_list.append((inodemax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + self.draw_periodic_connections_wrap( + left_list, + self.x, + self.y, + self.ghost_mesh_left.x, + self.ghost_mesh_left.y + ) + self.draw_periodic_connections_wrap( + right_list, + self.x, + self.y, + self.ghost_mesh_right.x, + self.ghost_mesh_right.y, + -1 + ) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/mesh/01/00_testanimation.py b/example/figure/1d/mesh/01/00_testanimation.py new file mode 100644 index 00000000..53ae992e --- /dev/null +++ b/example/figure/1d/mesh/01/00_testanimation.py @@ -0,0 +1,28 @@ +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import numpy as np + +# 创建图形和轴 +fig, ax = plt.subplots() +ax.set_xlim(0, 2*np.pi) +ax.set_ylim(-1, 1) + +# 初始化数据 +x = np.linspace(0, 2*np.pi, 1000) +line, = ax.plot(x, np.sin(x), color='blue') + +# 动画更新函数 +def animate(frame): + # 随着帧数增加,x 数据偏移,实现波形移动 + y = np.sin(x + frame / 10.0) + line.set_ydata(y) + return line, + +# 创建动画:100 帧,每帧间隔 20ms,支持 blit 优化(更快渲染) +ani = animation.FuncAnimation(fig, animate, frames=100, interval=20, blit=True) + +# 显示动画(在 Jupyter 中可用 plt.show(),否则保存为 GIF) +plt.show() + +# 可选:保存为 GIF 文件(需要 pillow 或 imagemagick) +# ani.save('sine_wave.gif', writer='pillow', fps=30) \ No newline at end of file diff --git a/example/figure/1d/mesh/01/01_cfd_grid_storage.py b/example/figure/1d/mesh/01/01_cfd_grid_storage.py new file mode 100644 index 00000000..9f686ecc --- /dev/null +++ b/example/figure/1d/mesh/01/01_cfd_grid_storage.py @@ -0,0 +1,214 @@ +""" +文件名: 01_cfd_grid_storage.py +功能: 绘制一维CFD基础网格与变量存储示意图 +包含: 顶点中心存储、单元中心存储、边界条件处理、有限差分格式示意 +""" + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14, + 'figure.dpi': 100 + }) + +def plot_cfd_grid_storage(): + """绘制一维CFD网格和变量存储方式对比""" + setup_plot_style() + fig = plt.figure(figsize=(15, 10)) + + # 创建网格 + n_cells = 5 + dx = 1.0 + x_vertices = np.linspace(0, n_cells*dx, n_cells + 1) + x_centers = (x_vertices[:-1] + x_vertices[1:]) / 2 + + # 1. 顶点中心存储 + ax1 = plt.subplot(2, 2, 1) + ax1.set_title("Vertex-centered Storage", fontsize=14, fontweight='bold', pad=20) + + # 绘制网格线 + for x in x_vertices: + ax1.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制顶点 + ax1.scatter(x_vertices, np.zeros_like(x_vertices), + s=120, color='red', zorder=5, edgecolors='black', linewidth=1.5) + + # 标记顶点 + for i, x in enumerate(x_vertices): + if i == 0: + label = f'Boundary\n$u_0$' + color = "orange" + elif i == len(x_vertices) - 1: + label = f'Boundary\n$u_{i}$' + color = "orange" + else: + label = f'Storage\n$u_{i}$' + color = "yellow" + + ax1.text(x, -0.2, label, ha='center', fontsize=10, va='top', + bbox=dict(boxstyle="round,pad=0.4", facecolor=color, alpha=0.8, edgecolor='black')) + + # 绘制控制体 + for i in range(len(x_vertices)-1): + center = (x_vertices[i] + x_vertices[i+1]) / 2 + rect = ax1.add_patch(Rectangle((x_vertices[i], -0.08), dx, 0.16, + alpha=0.2, color='blue', + edgecolor='blue', linewidth=1.5)) + ax1.text(center, 0.15, f'Cell {i}', ha='center', fontsize=11, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue", alpha=0.8)) + + ax1.set_xlim(-0.5, n_cells*dx + 0.5) + ax1.set_ylim(-0.35, 0.35) + ax1.set_xlabel('Position x') + ax1.set_ylabel('Variable Storage') + ax1.text(0.5, 0.95, 'Variables stored at vertices', transform=ax1.transAxes, + ha='center', fontsize=11, bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow")) + ax1.grid(True, alpha=0.3) + + # 2. 单元中心存储 + ax2 = plt.subplot(2, 2, 2) + ax2.set_title("Cell-centered Storage", fontsize=14, fontweight='bold', pad=20) + + # 绘制网格线 + for x in x_vertices: + ax2.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制单元中心 + ax2.scatter(x_centers, np.zeros_like(x_centers), + s=120, color='green', zorder=5, edgecolors='black', linewidth=1.5) + + # 标记变量 + for i, x in enumerate(x_centers): + label = f'Storage\n$u_{i}$' + ax2.text(x, -0.2, label, ha='center', fontsize=10, va='top', + bbox=dict(boxstyle="round,pad=0.4", facecolor="lightgreen", alpha=0.8, edgecolor='black')) + + # 绘制控制体 + for i, x in enumerate(x_vertices[:-1]): + rect = ax2.add_patch(Rectangle((x, -0.08), dx, 0.16, + alpha=0.2, color='orange', + edgecolor='orange', linewidth=1.5)) + ax2.text(x + dx/2, 0.15, f'Cell {i}', ha='center', fontsize=11, + bbox=dict(boxstyle="round,pad=0.3", facecolor="peachpuff", alpha=0.8)) + + # 标记边界 + ax2.axvline(x_vertices[0], color='red', linestyle='--', linewidth=2, alpha=0.7) + ax2.axvline(x_vertices[-1], color='red', linestyle='--', linewidth=2, alpha=0.7) + ax2.text(x_vertices[0], 0.25, 'Boundary', ha='center', color='red', fontsize=11, fontweight='bold') + ax2.text(x_vertices[-1], 0.25, 'Boundary', ha='center', color='red', fontsize=11, fontweight='bold') + + ax2.set_xlim(-0.5, n_cells*dx + 0.5) + ax2.set_ylim(-0.35, 0.35) + ax2.set_xlabel('Position x') + ax2.set_ylabel('Variable Storage') + ax2.text(0.5, 0.95, 'Variables stored at cell centers', transform=ax2.transAxes, + ha='center', fontsize=11, bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow")) + ax2.grid(True, alpha=0.3) + + # 3. 边界条件处理示意 + ax3 = plt.subplot(2, 2, 3) + ax3.set_title("Boundary Conditions and Ghost Cells", fontsize=14, fontweight='bold', pad=20) + + # 扩展网格显示虚拟点 + x_extended = np.linspace(-dx, (n_cells+1)*dx, n_cells + 4) + x_real = x_extended[1:-2] # 真实计算区域 + + # 绘制所有点 + ax3.scatter(x_extended, np.zeros_like(x_extended), s=80, color='gray', alpha=0.5) + ax3.scatter(x_real, np.zeros_like(x_real), s=120, color='blue', + edgecolors='black', linewidth=1.5, zorder=5) + + # 标记边界 + ax3.axvline(0, color='red', linestyle='-', linewidth=3, alpha=0.8) + ax3.axvline(n_cells*dx, color='red', linestyle='-', linewidth=3, alpha=0.8) + + # 填充虚拟点区域 + ax3.axvspan(-dx, 0, alpha=0.15, color='red', hatch='//') + ax3.axvspan(n_cells*dx, (n_cells+1)*dx, alpha=0.15, color='red', hatch='//') + + # 标记点类型 + ax3.text(-dx/2, 0.15, 'Ghost Cell', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.4", facecolor="pink", alpha=0.9, edgecolor='red')) + ax3.text(n_cells*dx + dx/2, 0.15, 'Ghost Cell', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.4", facecolor="pink", alpha=0.9, edgecolor='red')) + + # 标记计算区域 + ax3.axvspan(0, n_cells*dx, alpha=0.1, color='green') + ax3.text(n_cells*dx/2, -0.2, 'Computational Domain', ha='center', fontsize=12, + bbox=dict(boxstyle="round,pad=0.4", facecolor="lightgreen", alpha=0.8)) + + # 添加箭头示意边界条件 + ax3.annotate('BC: u=0', xy=(0, 0), xytext=(-1.2*dx, 0.25), + arrowprops=dict(arrowstyle="->", color='darkred', lw=2), + ha='center', fontsize=10, color='darkred', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax3.annotate('BC: ∂u/∂x=0', xy=(n_cells*dx, 0), + xytext=(n_cells*dx + 1.2*dx, 0.25), + arrowprops=dict(arrowstyle="->", color='darkred', lw=2), + ha='center', fontsize=10, color='darkred', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax3.set_xlim(-1.8*dx, (n_cells+1.8)*dx) + ax3.set_ylim(-0.3, 0.4) + ax3.set_xlabel('Position x') + ax3.set_ylabel('Domain') + ax3.grid(True, alpha=0.3) + + # 4. 数值格式示意 + ax4 = plt.subplot(2, 2, 4) + ax4.set_title("Finite Difference Schemes", fontsize=14, fontweight='bold', pad=20) + + # 创建示例数据 + x = np.linspace(0, 10, 50) + u = np.sin(x * 0.8) * np.exp(-0.1*x) + + # 选取几个点 + i = 25 + ax4.plot(x, u, 'b-', linewidth=3, alpha=0.5, label='Exact Solution') + ax4.scatter(x[i], u[i], s=200, color='red', zorder=5, + edgecolors='black', linewidth=1.5, label='Current point $u_i$') + ax4.scatter(x[i-1], u[i-1], s=150, color='green', zorder=4, + edgecolors='black', linewidth=1, label='Upstream $u_{i-1}$') + ax4.scatter(x[i+1], u[i+1], s=150, color='orange', zorder=4, + edgecolors='black', linewidth=1, label='Downstream $u_{i+1}$') + + # 绘制差分示意线 + ax4.plot([x[i-1], x[i+1]], [u[i-1], u[i+1]], 'k--', alpha=0.5, linewidth=1.5) + + # 标注差分格式 + ax4.annotate('Central Difference\n(2nd order)', xy=(x[i], u[i]), xytext=(x[i], u[i]+0.4), + arrowprops=dict(arrowstyle="->", color='blue', lw=2), + ha='center', fontsize=11, color='blue', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax4.annotate('Upwind Scheme\n(1st order)', xy=(x[i], u[i]), xytext=(x[i]-2.5, u[i]-0.3), + arrowprops=dict(arrowstyle="->", color='red', lw=2), + ha='center', fontsize=11, color='red', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax4.set_xlabel('Position x') + ax4.set_ylabel('Variable u') + ax4.legend(loc='upper right', fontsize=10) + ax4.grid(True, alpha=0.3) + + # 添加整体标题 + plt.suptitle('1D CFD Grid and Variable Storage Illustration', fontsize=16, fontweight='bold', y=1.02) + + plt.tight_layout() + plt.savefig('01_cfd_grid_storage.png', dpi=300, bbox_inches='tight', facecolor='white') + plt.show() + +if __name__ == "__main__": + plot_cfd_grid_storage() \ No newline at end of file diff --git a/example/figure/1d/mesh/01/02_convection_schemes.py b/example/figure/1d/mesh/01/02_convection_schemes.py new file mode 100644 index 00000000..a2b2c413 --- /dev/null +++ b/example/figure/1d/mesh/01/02_convection_schemes.py @@ -0,0 +1,126 @@ +""" +文件名: 02_convection_schemes.py +功能: 绘制一维对流方程不同数值格式对比 +包含: 迎风格式、中心差分格式、QUICK格式的比较 +""" + +import numpy as np +import matplotlib.pyplot as plt + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14 + }) + +def plot_convection_schemes(): + """绘制不同对流格式示意图""" + setup_plot_style() + + # 创建数据 + x = np.linspace(0, 2*np.pi, 100) + u_exact = np.sin(x) + + # 添加数值噪声模拟数值解 + np.random.seed(42) + u_upwind = u_exact + 0.1*np.random.randn(len(x)) # 迎风格式(有耗散) + u_central = u_exact + 0.05*np.random.randn(len(x)) # 中心差分(有振荡) + u_quick = u_exact + 0.02*np.random.randn(len(x)) # QUICK格式 + + fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + + # 1. 迎风格式 + ax = axes[0, 0] + ax.plot(x, u_exact, 'k-', linewidth=3, alpha=0.7, label='Exact Solution') + ax.plot(x, u_upwind, 'r--', linewidth=2, marker='o', markersize=4, + markevery=5, label='Upwind Scheme') + ax.fill_between(x, u_exact-0.15, u_exact+0.15, alpha=0.1, color='gray') + ax.set_title('Upwind Scheme', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + ax.annotate('Numerical\nDissipation', xy=(3, 0), xytext=(4, -0.8), + arrowprops=dict(arrowstyle="->", color='red'), + fontsize=10, color='red') + + # 2. 中心差分格式 + ax = axes[0, 1] + ax.plot(x, u_exact, 'k-', linewidth=3, alpha=0.7, label='Exact Solution') + ax.plot(x, u_central, 'b--', linewidth=2, marker='s', markersize=4, + markevery=5, label='Central Difference') + ax.set_title('Central Difference Scheme', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + ax.annotate('Numerical\nOscillation', xy=(1.5, 0.5), xytext=(0.5, 0.8), + arrowprops=dict(arrowstyle="->", color='blue'), + fontsize=10, color='blue') + + # 3. QUICK格式 + ax = axes[1, 0] + ax.plot(x, u_exact, 'k-', linewidth=3, alpha=0.7, label='Exact Solution') + ax.plot(x, u_quick, 'g--', linewidth=2, marker='^', markersize=4, + markevery=5, label='QUICK Scheme') + ax.set_title('QUICK Scheme (3rd Order)', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 4. 格式示意 + ax = axes[1, 1] + ax.set_title('Grid Point Dependencies', fontsize=12, fontweight='bold') + + # 绘制网格点 + points_x = np.array([0, 1, 2, 3, 4]) + points_y = np.zeros_like(points_x) + + ax.scatter(points_x, points_y, s=200, color='gray') + + # 标记点 + labels = ['$u_{i-2}$', '$u_{i-1}$', '$u_i$', '$u_{i+1}$', '$u_{i+2}$'] + for i, (x_pos, label) in enumerate(zip(points_x, labels)): + ax.text(x_pos, 0.1, label, ha='center', fontsize=12, fontweight='bold') + + # 迎风格式依赖 + ax.annotate('', xy=(2, 0), xytext=(1, 0), + arrowprops=dict(arrowstyle='<-', color='red', lw=2)) + ax.text(1.5, -0.15, 'Upwind', ha='center', color='red', fontweight='bold') + + # 中心差分依赖 + ax.annotate('', xy=(2, 0), xytext=(1, -0.05), + arrowprops=dict(arrowstyle='<-', color='blue', lw=2)) + ax.annotate('', xy=(2, 0), xytext=(3, -0.05), + arrowprops=dict(arrowstyle='<-', color='blue', lw=2)) + ax.text(2, -0.2, 'Central', ha='center', color='blue', fontweight='bold') + + # QUICK格式依赖 + ax.annotate('', xy=(2, 0), xytext=(0, 0.05), + arrowprops=dict(arrowstyle='<-', color='green', lw=2, alpha=0.7)) + ax.annotate('', xy=(2, 0), xytext=(1, 0.05), + arrowprops=dict(arrowstyle='<-', color='green', lw=2, alpha=0.7)) + ax.annotate('', xy=(2, 0), xytext=(3, 0.05), + arrowprops=dict(arrowstyle='<-', color='green', lw=2, alpha=0.7)) + ax.text(2, 0.2, 'QUICK', ha='center', color='green', fontweight='bold') + + ax.set_xlim(-0.5, 4.5) + ax.set_ylim(-0.3, 0.3) + ax.set_xlabel('Grid Point Index') + ax.grid(True, alpha=0.3) + ax.set_yticks([]) + + plt.suptitle('Comparison of Convection Schemes for 1D CFD', fontsize=14, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('02_convection_schemes.png', dpi=300, bbox_inches='tight') + plt.show() + +if __name__ == "__main__": + plot_convection_schemes() \ No newline at end of file diff --git a/example/figure/1d/mesh/01/03_interpolation_methods.py b/example/figure/1d/mesh/01/03_interpolation_methods.py new file mode 100644 index 00000000..44eaf808 --- /dev/null +++ b/example/figure/1d/mesh/01/03_interpolation_methods.py @@ -0,0 +1,119 @@ +""" +文件名: 03_interpolation_methods.py +功能: 绘制不同插值方法对比 +包含: 线性插值、二次插值、三次样条插值的比较 +""" + +import numpy as np +import matplotlib.pyplot as plt +from scipy import interpolate + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14 + }) + +def plot_interpolation_methods(): + """绘制不同插值方法对比""" + setup_plot_style() + + # 创建粗网格和细网格 + x_coarse = np.linspace(0, 10, 6) + u_coarse = np.sin(x_coarse * 0.8) + + x_fine = np.linspace(0, 10, 100) + u_exact = np.sin(x_fine * 0.8) + + # 不同插值方法 + # 线性插值 + f_linear = interpolate.interp1d(x_coarse, u_coarse, kind='linear') + u_linear = f_linear(x_fine) + + # 二次插值 + f_quadratic = interpolate.interp1d(x_coarse, u_coarse, kind='quadratic') + u_quadratic = f_quadratic(x_fine) + + # 三次样条插值 + f_cubic = interpolate.CubicSpline(x_coarse, u_coarse) + u_cubic = f_cubic(x_fine) + + fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + + # 1. 线性插值 + ax = axes[0, 0] + ax.plot(x_fine, u_exact, 'k-', alpha=0.3, linewidth=3, label='Exact Solution') + ax.plot(x_fine, u_linear, 'r--', linewidth=2, label='Linear Interpolation') + ax.scatter(x_coarse, u_coarse, s=100, color='blue', zorder=5, label='Known Points') + ax.set_title('Linear Interpolation', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 2. 二次插值 + ax = axes[0, 1] + ax.plot(x_fine, u_exact, 'k-', alpha=0.3, linewidth=3, label='Exact Solution') + ax.plot(x_fine, u_quadratic, 'g--', linewidth=2, label='Quadratic Interpolation') + ax.scatter(x_coarse, u_coarse, s=100, color='blue', zorder=5, label='Known Points') + ax.set_title('Quadratic Interpolation', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 3. 三次样条插值 + ax = axes[1, 0] + ax.plot(x_fine, u_exact, 'k-', alpha=0.3, linewidth=3, label='Exact Solution') + ax.plot(x_fine, u_cubic, 'b--', linewidth=2, label='Cubic Spline') + ax.scatter(x_coarse, u_coarse, s=100, color='blue', zorder=5, label='Known Points') + ax.set_title('Cubic Spline Interpolation', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 4. 误差比较 + ax = axes[1, 1] + errors = { + 'Linear': np.abs(u_linear - u_exact), + 'Quadratic': np.abs(u_quadratic - u_exact), + 'Cubic Spline': np.abs(u_cubic - u_exact) + } + + x_pos = np.arange(len(errors)) + mean_errors = [np.mean(err) for err in errors.values()] + max_errors = [np.max(err) for err in errors.values()] + + width = 0.35 + ax.bar(x_pos - width/2, mean_errors, width, label='Mean Error', color='skyblue') + ax.bar(x_pos + width/2, max_errors, width, label='Max Error', color='salmon') + + ax.set_xlabel('Interpolation Method') + ax.set_ylabel('Error') + ax.set_title('Interpolation Error Comparison', fontsize=12, fontweight='bold') + ax.set_xticks(x_pos) + ax.set_xticklabels(list(errors.keys())) + ax.legend() + ax.grid(True, alpha=0.3, axis='y') + + # 添加数值标签 + for i, (mean_err, max_err) in enumerate(zip(mean_errors, max_errors)): + ax.text(i - width/2, mean_err + 0.001, f'{mean_err:.3f}', + ha='center', va='bottom', fontsize=9) + ax.text(i + width/2, max_err + 0.001, f'{max_err:.3f}', + ha='center', va='bottom', fontsize=9) + + plt.suptitle('Comparison of Interpolation Methods for CFD', fontsize=14, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('03_interpolation_methods.png', dpi=300, bbox_inches='tight') + plt.show() + +if __name__ == "__main__": + plot_interpolation_methods() \ No newline at end of file diff --git a/example/figure/1d/mesh/01/04_cfd_animation.py b/example/figure/1d/mesh/01/04_cfd_animation.py new file mode 100644 index 00000000..566d7d3b --- /dev/null +++ b/example/figure/1d/mesh/01/04_cfd_animation.py @@ -0,0 +1,372 @@ +""" +文件名: 04_cfd_animation.py +功能: 创建CFD计算过程动画 +修复: 分离保存动画和显示动画的过程,避免AttributeError +""" + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation, PillowWriter, FFMpegWriter +import warnings +import sys +import os + +warnings.filterwarnings('ignore') + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14, + 'figure.dpi': 100, + 'savefig.dpi': 150, + 'animation.embed_limit': 100 # 增加嵌入限制 + }) + +def simulate_convection_equation(): + """模拟对流方程的解""" + # 设置网格和参数 + L = 10.0 # 计算域长度 + nx = 100 # 网格点数 + dx = L / nx + x = np.linspace(0, L, nx) + + # 时间参数 + nt = 80 # 减少帧数以加快速度 + dt = 0.05 + + # 初始化变量 + u_exact = np.zeros((nt, nx)) + u_numerical = np.zeros((nt, nx)) + + # 初始条件:高斯波包 + u0 = np.exp(-(x - L/4)**2 / 0.5) * np.sin(3 * x) + + # 生成精确解(对流方程) + c = 0.5 # 对流速度 + for n in range(nt): + # 精确解:简单对流 + shift = c * n * dt + u_exact[n, :] = np.exp(-(x - shift - L/4)**2 / 0.5) * np.sin(3 * (x - shift)) + + # 数值解:使用一阶迎风格式 + if n == 0: + u_numerical[n, :] = u0 + else: + for i in range(1, nx): + # 一阶迎风格式 + u_numerical[n, i] = u_numerical[n-1, i] - c*dt/dx * ( + u_numerical[n-1, i] - u_numerical[n-1, i-1]) + # 边界条件:左侧固定为零 + u_numerical[n, 0] = 0.0 + + return x, u_exact, u_numerical, nt, dt + +def save_animation_only(): + """仅保存动画,不显示""" + print("正在创建并保存动画...") + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 创建图形 + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + + # 初始化线条 + line_exact, = ax1.plot([], [], 'b-', linewidth=2, label='Exact Solution') + line_num, = ax2.plot([], [], 'r-', linewidth=2, label='Numerical Solution') + line_num_dots, = ax2.plot([], [], 'ro', markersize=4, alpha=0.5, markevery=5) + + # 设置坐标轴 + for ax in [ax1, ax2]: + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('Variable u') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + ax1.set_title('Exact Solution of Convection Equation') + ax2.set_title('Numerical Solution (First-order Upwind Scheme)') + + fig.suptitle('1D Convection Equation Simulation', fontsize=16, fontweight='bold', y=0.98) + + # 添加时间文本 + time_text = fig.text(0.5, 0.95, '', ha='center', fontsize=12, + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8)) + + # 初始化函数 + def init(): + line_exact.set_data([], []) + line_num.set_data([], []) + line_num_dots.set_data([], []) + time_text.set_text('') + return line_exact, line_num, line_num_dots, time_text + + # 动画更新函数 + def update(frame): + # 更新精确解 + line_exact.set_data(x, u_exact[frame, :]) + + # 更新数值解 + line_num.set_data(x, u_numerical[frame, :]) + line_num_dots.set_data(x[::5], u_numerical[frame, ::5]) + + # 更新时间文本 + time_text.set_text(f'Time: {frame*dt:.2f} s') + + return line_exact, line_num, line_num_dots, time_text + + # 创建动画对象 + ani = FuncAnimation(fig, update, frames=nt, + init_func=init, blit=True) + + # 尝试保存动画 + saved = False + + # 首先尝试保存为mp4 + try: + print("尝试保存为MP4格式...") + writer = FFMpegWriter(fps=10, metadata=dict(artist='CFD Visualization'), bitrate=1800) + ani.save('04_cfd_animation.mp4', writer=writer) + print("✓ 动画已成功保存为 '04_cfd_animation.mp4'") + saved = True + except Exception as e: + print(f"MP4保存失败: {e}") + + # 如果mp4失败,尝试保存为gif + if not saved: + try: + print("尝试保存为GIF格式...") + writer = PillowWriter(fps=10) + ani.save('04_cfd_animation.gif', writer=writer) + print("✓ 动画已保存为 '04_cfd_animation.gif'") + saved = True + except Exception as e: + print(f"GIF保存失败: {e}") + + # 关闭图形以释放资源 + plt.close(fig) + + if saved: + print("\n动画保存完成!文件已生成。") + print("您可以使用视频播放器查看保存的动画文件。") + else: + print("\n无法保存动画,将创建静态图像...") + create_static_frames() + + return saved + +def create_static_frames(): + """创建静态的关键帧图像""" + print("正在创建静态关键帧图像...") + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 选择几个关键帧 + key_frames = [0, nt//4, nt//2, 3*nt//4, nt-1] + frame_names = ['Initial', 'Quarter', 'Half', 'Three Quarters', 'Final'] + + fig, axes = plt.subplots(2, 3, figsize=(15, 8)) + axes = axes.flatten() + + for idx, (frame, name) in enumerate(zip(key_frames, frame_names)): + if idx >= len(axes): + break + + ax = axes[idx] + ax.plot(x, u_exact[frame, :], 'b-', linewidth=2, label='Exact', alpha=0.7) + ax.plot(x, u_numerical[frame, :], 'r--', linewidth=2, label='Numerical') + ax.scatter(x[::10], u_numerical[frame, ::10], s=30, color='red', alpha=0.5) + + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('u') + ax.set_title(f'{name} (t={frame*dt:.2f}s)') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + # 第6个子图:误差随时间变化 + ax = axes[5] + errors = np.abs(u_numerical - u_exact) + mean_errors = np.mean(errors, axis=1) + max_errors = np.max(errors, axis=1) + + time = np.arange(nt) * dt + ax.plot(time, mean_errors, 'b-', label='Mean Error') + ax.plot(time, max_errors, 'r-', label='Max Error') + ax.fill_between(time, 0, mean_errors, alpha=0.3, color='blue') + ax.fill_between(time, 0, max_errors, alpha=0.1, color='red') + + ax.set_xlabel('Time') + ax.set_ylabel('Error') + ax.set_title('Numerical Error Evolution') + ax.grid(True, alpha=0.3) + ax.legend() + + plt.suptitle('1D Convection Equation: Key Frames and Error Analysis', + fontsize=16, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('04_cfd_static_frames.png', dpi=300, bbox_inches='tight') + print("✓ 静态关键帧已保存为 '04_cfd_static_frames.png'") + plt.close(fig) + + return True + +def show_animation_only(): + """仅显示动画,不保存""" + print("正在创建动画以供显示...") + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 创建新的图形 + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + + # 初始化线条 + line_exact, = ax1.plot([], [], 'b-', linewidth=2, label='Exact Solution') + line_num, = ax2.plot([], [], 'r-', linewidth=2, label='Numerical Solution') + line_num_dots, = ax2.plot([], [], 'ro', markersize=4, alpha=0.5, markevery=5) + + # 设置坐标轴 + for ax in [ax1, ax2]: + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('Variable u') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + ax1.set_title('Exact Solution of Convection Equation') + ax2.set_title('Numerical Solution (First-order Upwind Scheme)') + + fig.suptitle('1D Convection Equation Simulation (Live)', fontsize=16, fontweight='bold', y=0.98) + + # 添加时间文本 + time_text = fig.text(0.5, 0.95, '', ha='center', fontsize=12, + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8)) + + # 初始化函数 + def init(): + line_exact.set_data([], []) + line_num.set_data([], []) + line_num_dots.set_data([], []) + time_text.set_text('') + return line_exact, line_num, line_num_dots, time_text + + # 动画更新函数 + def update(frame): + # 更新精确解 + line_exact.set_data(x, u_exact[frame, :]) + + # 更新数值解 + line_num.set_data(x, u_numerical[frame, :]) + line_num_dots.set_data(x[::5], u_numerical[frame, ::5]) + + # 更新时间文本 + time_text.set_text(f'Time: {frame*dt:.2f} s | Frame: {frame}/{nt}') + + return line_exact, line_num, line_num_dots, time_text + + # 创建动画 + ani = FuncAnimation(fig, update, frames=nt, + init_func=init, blit=True, interval=100, repeat=False) + + print("动画准备就绪,显示窗口...") + print("提示:关闭窗口以继续程序。") + + plt.tight_layout() + plt.show() + + return ani + +def check_ffmpeg_available(): + """检查ffmpeg是否可用""" + try: + import subprocess + result = subprocess.run(['ffmpeg', '-version'], + capture_output=True, text=True, shell=True) + return result.returncode == 0 + except: + return False + +def main(): + """主函数""" + print("=" * 60) + print("1D CFD 动画演示程序") + print("=" * 60) + + # 检查ffmpeg + has_ffmpeg = check_ffmpeg_available() + print(f"FFmpeg可用: {'✓' if has_ffmpeg else '✗'}") + + print("\n选项:") + print("1. 保存动画为视频文件(不显示)") + print("2. 显示动画(不保存)") + print("3. 创建静态关键帧图像") + + try: + choice = int(input("\n请选择 (1-3, 默认=1): ") or "1") + except: + choice = 1 + + setup_plot_style() + + if choice == 1: + print("\n" + "=" * 50) + print("选项1: 保存动画为视频文件") + print("=" * 50) + save_animation_only() + + elif choice == 2: + print("\n" + "=" * 50) + print("选项2: 显示动画") + print("=" * 50) + print("注意:动画将在新窗口中显示") + print("关闭窗口后程序将继续运行") + show_animation_only() + + elif choice == 3: + print("\n" + "=" * 50) + print("选项3: 创建静态关键帧图像") + print("=" * 50) + create_static_frames() + + else: + print("\n无效选择,使用默认选项...") + save_animation_only() + + print("\n" + "=" * 60) + print("程序执行完成!") + print("生成的文件:") + + # 列出生成的文件 + files_to_check = [ + ('04_cfd_animation.mp4', '动画视频文件'), + ('04_cfd_animation.gif', '动画GIF文件'), + ('04_cfd_static_frames.png', '静态关键帧图像') + ] + + for filename, description in files_to_check: + if os.path.exists(filename): + file_size = os.path.getsize(filename) / 1024 # KB + print(f" ✓ {filename} - {description} ({file_size:.1f} KB)") + else: + print(f" ✗ {filename} - 未生成") + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n\n程序被用户中断。") + except Exception as e: + print(f"\n程序执行出错: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/example/figure/1d/mesh/01/04_cfd_animationBAK.py b/example/figure/1d/mesh/01/04_cfd_animationBAK.py new file mode 100644 index 00000000..f3c274c9 --- /dev/null +++ b/example/figure/1d/mesh/01/04_cfd_animationBAK.py @@ -0,0 +1,299 @@ +""" +文件名: 04_cfd_animation.py +功能: 创建CFD计算过程动画 +包含: 波动方程的数值解演化过程 +修复: 解决了动画保存后plt.show()的AttributeError问题 +""" + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import warnings +warnings.filterwarnings('ignore') + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 12, + 'axes.titlesize': 14, + 'axes.labelsize': 12, + 'figure.titlesize': 16 + }) + +def simulate_convection_equation(): + """模拟对流方程的解""" + # 设置网格和参数 + L = 10.0 # 计算域长度 + nx = 100 # 网格点数 + dx = L / nx + x = np.linspace(0, L, nx) + + # 时间参数 + nt = 100 # 减少帧数以加快速度 + dt = 0.05 + + # 初始化变量 + u_exact = np.zeros((nt, nx)) + u_numerical = np.zeros((nt, nx)) + + # 初始条件:高斯波包 + u0 = np.exp(-(x - L/4)**2 / 0.5) * np.sin(3 * x) + + # 生成精确解(对流方程) + c = 0.5 # 对流速度 + for n in range(nt): + # 精确解:简单对流 + shift = c * n * dt + u_exact[n, :] = np.exp(-(x - shift - L/4)**2 / 0.5) * np.sin(3 * (x - shift)) + + # 数值解:使用一阶迎风格式 + if n == 0: + u_numerical[n, :] = u0 + else: + for i in range(1, nx): + # 一阶迎风格式 + u_numerical[n, i] = u_numerical[n-1, i] - c*dt/dx * ( + u_numerical[n-1, i] - u_numerical[n-1, i-1]) + # 边界条件:左侧固定为零 + u_numerical[n, 0] = 0.0 + + return x, u_exact, u_numerical, nt, dt + +def create_cfd_animation(save_video=True, show_plot=True): + """创建CFD计算过程动画""" + setup_plot_style() + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 创建图形 + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + + # 初始化线条 + line_exact, = ax1.plot([], [], 'b-', linewidth=2, label='Exact Solution') + line_num, = ax2.plot([], [], 'r-', linewidth=2, label='Numerical Solution') + line_num_dots, = ax2.plot([], [], 'ro', markersize=4, alpha=0.5, markevery=5) + + # 设置坐标轴 + for ax in [ax1, ax2]: + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('Variable u') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + ax1.set_title('Exact Solution of Convection Equation') + ax2.set_title('Numerical Solution (First-order Upwind Scheme)') + + fig.suptitle('1D Convection Equation Simulation', fontsize=16, fontweight='bold', y=0.98) + + # 添加时间文本 + time_text = fig.text(0.5, 0.95, '', ha='center', fontsize=12, + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8)) + + # 添加说明文本 + info_text = fig.text(0.5, 0.02, + 'Convection velocity: c = 0.5 | CFL number: ~0.5', + ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.7)) + + # 初始化函数 + def init(): + line_exact.set_data([], []) + line_num.set_data([], []) + line_num_dots.set_data([], []) + time_text.set_text('') + return line_exact, line_num, line_num_dots, time_text + + # 动画更新函数 + def update(frame): + # 更新精确解 + line_exact.set_data(x, u_exact[frame, :]) + + # 更新数值解 + line_num.set_data(x, u_numerical[frame, :]) + # 每隔5个点显示一个标记点 + line_num_dots.set_data(x[::5], u_numerical[frame, ::5]) + + # 更新时间文本 + time_text.set_text(f'Time: {frame*dt:.2f} s | Frame: {frame}/{nt}') + + return line_exact, line_num, line_num_dots, time_text + + # 创建动画 + ani = animation.FuncAnimation(fig, update, frames=nt, + init_func=init, blit=True, interval=100, repeat=False) + + # 保存动画(可选) + if save_video: + try: + print("正在保存动画...") + # 尝试使用不同的写入器 + try: + ani.save('04_cfd_animation.mp4', writer='ffmpeg', fps=10, dpi=150) + print("动画已保存为 '04_cfd_animation.mp4'") + except: + # 如果ffmpeg不可用,尝试保存为gif + ani.save('04_cfd_animation.gif', writer='pillow', fps=10, dpi=150) + print("动画已保存为 '04_cfd_animation.gif' (使用pillow写入器)") + except Exception as e: + print(f"保存动画失败: {e}") + print("请确保已安装必要的视频编码器(如ffmpeg)或pillow库") + + # 显示动画(可选) + if show_plot: + try: + plt.tight_layout() + plt.show() + except Exception as e: + print(f"显示动画时出错: {e}") + print("这可能是因为图形窗口已关闭,但这是正常的。") + + return ani + +def create_static_frames(): + """创建静态的关键帧图像,作为替代方案""" + setup_plot_style() + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 选择几个关键帧 + key_frames = [0, nt//4, nt//2, 3*nt//4, nt-1] + frame_names = ['Initial', 'Quarter', 'Half', 'Three Quarters', 'Final'] + + fig, axes = plt.subplots(2, 3, figsize=(15, 8)) + axes = axes.flatten() + + for idx, (frame, name) in enumerate(zip(key_frames, frame_names)): + if idx >= len(axes): + break + + ax = axes[idx] + ax.plot(x, u_exact[frame, :], 'b-', linewidth=2, label='Exact', alpha=0.7) + ax.plot(x, u_numerical[frame, :], 'r--', linewidth=2, label='Numerical') + ax.scatter(x[::10], u_numerical[frame, ::10], s=30, color='red', alpha=0.5) + + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('u') + ax.set_title(f'{name} (t={frame*dt:.2f}s)') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + # 第6个子图:误差随时间变化 + ax = axes[5] + errors = np.abs(u_numerical - u_exact) + mean_errors = np.mean(errors, axis=1) + max_errors = np.max(errors, axis=1) + + time = np.arange(nt) * dt + ax.plot(time, mean_errors, 'b-', label='Mean Error') + ax.plot(time, max_errors, 'r-', label='Max Error') + ax.fill_between(time, 0, mean_errors, alpha=0.3, color='blue') + ax.fill_between(time, 0, max_errors, alpha=0.1, color='red') + + ax.set_xlabel('Time') + ax.set_ylabel('Error') + ax.set_title('Numerical Error Evolution') + ax.grid(True, alpha=0.3) + ax.legend() + + plt.suptitle('1D Convection Equation: Key Frames and Error Analysis', + fontsize=16, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('04_cfd_static_frames.png', dpi=300, bbox_inches='tight') + print("静态关键帧已保存为 '04_cfd_static_frames.png'") + plt.show() + +def create_simple_animation(): + """创建一个更简单的动画,避免复杂问题""" + setup_plot_style() + + # 简单示例:波的传播 + x = np.linspace(0, 10, 200) + t = np.linspace(0, 4*np.pi, 100) + + fig, ax = plt.subplots(figsize=(10, 6)) + + # 创建初始线 + line, = ax.plot([], [], 'b-', linewidth=2) + + # 设置图形 + ax.set_xlim(0, 10) + ax.set_ylim(-1.5, 1.5) + ax.set_xlabel('Position x') + ax.set_ylabel('Amplitude u') + ax.set_title('Wave Propagation in 1D Domain') + ax.grid(True, alpha=0.3) + + # 初始化函数 + def init(): + line.set_data([], []) + return line, + + # 更新函数 + def update(i): + y = np.sin(2*np.pi*(x/5 - t[i]/5)) * np.exp(-0.05*x) + line.set_data(x, y) + ax.set_title(f'Wave Propagation (t = {t[i]:.2f})') + return line, + + # 创建动画 + ani = animation.FuncAnimation(fig, update, frames=len(t), + init_func=init, blit=True, interval=50) + + # 保存为gif + try: + ani.save('04_simple_wave.gif', writer='pillow', fps=15) + print("简单动画已保存为 '04_simple_wave.gif'") + except Exception as e: + print(f"保存简单动画失败: {e}") + + plt.show() + return ani + +if __name__ == "__main__": + print("=" * 60) + print("1D CFD 动画演示程序") + print("=" * 60) + print("\n选项:") + print("1. 创建完整动画(可能有问题)") + print("2. 创建静态关键帧图像") + print("3. 创建简单波动动画") + + try: + choice = int(input("\n请选择 (1-3, 默认=2): ") or "2") + except: + choice = 2 + + if choice == 1: + print("\n正在创建完整动画...") + print("注意:保存视频需要ffmpeg或pillow库") + print("如果失败,将自动回退到静态图像") + + # 先尝试创建静态图像 + create_static_frames() + + # 然后尝试动画 + try: + ani = create_cfd_animation(save_video=True, show_plot=True) + except Exception as e: + print(f"\n动画创建失败: {e}") + print("已创建静态图像作为替代。") + + elif choice == 2: + print("\n正在创建静态关键帧图像...") + create_static_frames() + + elif choice == 3: + print("\n正在创建简单波动动画...") + create_simple_animation() + + else: + print("\n无效选择,创建静态关键帧图像...") + create_static_frames() + + print("\n程序执行完成!") \ No newline at end of file diff --git a/example/figure/1d/mesh/01/04_cfd_animationOld.py b/example/figure/1d/mesh/01/04_cfd_animationOld.py new file mode 100644 index 00000000..f3c274c9 --- /dev/null +++ b/example/figure/1d/mesh/01/04_cfd_animationOld.py @@ -0,0 +1,299 @@ +""" +文件名: 04_cfd_animation.py +功能: 创建CFD计算过程动画 +包含: 波动方程的数值解演化过程 +修复: 解决了动画保存后plt.show()的AttributeError问题 +""" + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import warnings +warnings.filterwarnings('ignore') + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 12, + 'axes.titlesize': 14, + 'axes.labelsize': 12, + 'figure.titlesize': 16 + }) + +def simulate_convection_equation(): + """模拟对流方程的解""" + # 设置网格和参数 + L = 10.0 # 计算域长度 + nx = 100 # 网格点数 + dx = L / nx + x = np.linspace(0, L, nx) + + # 时间参数 + nt = 100 # 减少帧数以加快速度 + dt = 0.05 + + # 初始化变量 + u_exact = np.zeros((nt, nx)) + u_numerical = np.zeros((nt, nx)) + + # 初始条件:高斯波包 + u0 = np.exp(-(x - L/4)**2 / 0.5) * np.sin(3 * x) + + # 生成精确解(对流方程) + c = 0.5 # 对流速度 + for n in range(nt): + # 精确解:简单对流 + shift = c * n * dt + u_exact[n, :] = np.exp(-(x - shift - L/4)**2 / 0.5) * np.sin(3 * (x - shift)) + + # 数值解:使用一阶迎风格式 + if n == 0: + u_numerical[n, :] = u0 + else: + for i in range(1, nx): + # 一阶迎风格式 + u_numerical[n, i] = u_numerical[n-1, i] - c*dt/dx * ( + u_numerical[n-1, i] - u_numerical[n-1, i-1]) + # 边界条件:左侧固定为零 + u_numerical[n, 0] = 0.0 + + return x, u_exact, u_numerical, nt, dt + +def create_cfd_animation(save_video=True, show_plot=True): + """创建CFD计算过程动画""" + setup_plot_style() + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 创建图形 + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + + # 初始化线条 + line_exact, = ax1.plot([], [], 'b-', linewidth=2, label='Exact Solution') + line_num, = ax2.plot([], [], 'r-', linewidth=2, label='Numerical Solution') + line_num_dots, = ax2.plot([], [], 'ro', markersize=4, alpha=0.5, markevery=5) + + # 设置坐标轴 + for ax in [ax1, ax2]: + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('Variable u') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + ax1.set_title('Exact Solution of Convection Equation') + ax2.set_title('Numerical Solution (First-order Upwind Scheme)') + + fig.suptitle('1D Convection Equation Simulation', fontsize=16, fontweight='bold', y=0.98) + + # 添加时间文本 + time_text = fig.text(0.5, 0.95, '', ha='center', fontsize=12, + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8)) + + # 添加说明文本 + info_text = fig.text(0.5, 0.02, + 'Convection velocity: c = 0.5 | CFL number: ~0.5', + ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.7)) + + # 初始化函数 + def init(): + line_exact.set_data([], []) + line_num.set_data([], []) + line_num_dots.set_data([], []) + time_text.set_text('') + return line_exact, line_num, line_num_dots, time_text + + # 动画更新函数 + def update(frame): + # 更新精确解 + line_exact.set_data(x, u_exact[frame, :]) + + # 更新数值解 + line_num.set_data(x, u_numerical[frame, :]) + # 每隔5个点显示一个标记点 + line_num_dots.set_data(x[::5], u_numerical[frame, ::5]) + + # 更新时间文本 + time_text.set_text(f'Time: {frame*dt:.2f} s | Frame: {frame}/{nt}') + + return line_exact, line_num, line_num_dots, time_text + + # 创建动画 + ani = animation.FuncAnimation(fig, update, frames=nt, + init_func=init, blit=True, interval=100, repeat=False) + + # 保存动画(可选) + if save_video: + try: + print("正在保存动画...") + # 尝试使用不同的写入器 + try: + ani.save('04_cfd_animation.mp4', writer='ffmpeg', fps=10, dpi=150) + print("动画已保存为 '04_cfd_animation.mp4'") + except: + # 如果ffmpeg不可用,尝试保存为gif + ani.save('04_cfd_animation.gif', writer='pillow', fps=10, dpi=150) + print("动画已保存为 '04_cfd_animation.gif' (使用pillow写入器)") + except Exception as e: + print(f"保存动画失败: {e}") + print("请确保已安装必要的视频编码器(如ffmpeg)或pillow库") + + # 显示动画(可选) + if show_plot: + try: + plt.tight_layout() + plt.show() + except Exception as e: + print(f"显示动画时出错: {e}") + print("这可能是因为图形窗口已关闭,但这是正常的。") + + return ani + +def create_static_frames(): + """创建静态的关键帧图像,作为替代方案""" + setup_plot_style() + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 选择几个关键帧 + key_frames = [0, nt//4, nt//2, 3*nt//4, nt-1] + frame_names = ['Initial', 'Quarter', 'Half', 'Three Quarters', 'Final'] + + fig, axes = plt.subplots(2, 3, figsize=(15, 8)) + axes = axes.flatten() + + for idx, (frame, name) in enumerate(zip(key_frames, frame_names)): + if idx >= len(axes): + break + + ax = axes[idx] + ax.plot(x, u_exact[frame, :], 'b-', linewidth=2, label='Exact', alpha=0.7) + ax.plot(x, u_numerical[frame, :], 'r--', linewidth=2, label='Numerical') + ax.scatter(x[::10], u_numerical[frame, ::10], s=30, color='red', alpha=0.5) + + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('u') + ax.set_title(f'{name} (t={frame*dt:.2f}s)') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + # 第6个子图:误差随时间变化 + ax = axes[5] + errors = np.abs(u_numerical - u_exact) + mean_errors = np.mean(errors, axis=1) + max_errors = np.max(errors, axis=1) + + time = np.arange(nt) * dt + ax.plot(time, mean_errors, 'b-', label='Mean Error') + ax.plot(time, max_errors, 'r-', label='Max Error') + ax.fill_between(time, 0, mean_errors, alpha=0.3, color='blue') + ax.fill_between(time, 0, max_errors, alpha=0.1, color='red') + + ax.set_xlabel('Time') + ax.set_ylabel('Error') + ax.set_title('Numerical Error Evolution') + ax.grid(True, alpha=0.3) + ax.legend() + + plt.suptitle('1D Convection Equation: Key Frames and Error Analysis', + fontsize=16, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('04_cfd_static_frames.png', dpi=300, bbox_inches='tight') + print("静态关键帧已保存为 '04_cfd_static_frames.png'") + plt.show() + +def create_simple_animation(): + """创建一个更简单的动画,避免复杂问题""" + setup_plot_style() + + # 简单示例:波的传播 + x = np.linspace(0, 10, 200) + t = np.linspace(0, 4*np.pi, 100) + + fig, ax = plt.subplots(figsize=(10, 6)) + + # 创建初始线 + line, = ax.plot([], [], 'b-', linewidth=2) + + # 设置图形 + ax.set_xlim(0, 10) + ax.set_ylim(-1.5, 1.5) + ax.set_xlabel('Position x') + ax.set_ylabel('Amplitude u') + ax.set_title('Wave Propagation in 1D Domain') + ax.grid(True, alpha=0.3) + + # 初始化函数 + def init(): + line.set_data([], []) + return line, + + # 更新函数 + def update(i): + y = np.sin(2*np.pi*(x/5 - t[i]/5)) * np.exp(-0.05*x) + line.set_data(x, y) + ax.set_title(f'Wave Propagation (t = {t[i]:.2f})') + return line, + + # 创建动画 + ani = animation.FuncAnimation(fig, update, frames=len(t), + init_func=init, blit=True, interval=50) + + # 保存为gif + try: + ani.save('04_simple_wave.gif', writer='pillow', fps=15) + print("简单动画已保存为 '04_simple_wave.gif'") + except Exception as e: + print(f"保存简单动画失败: {e}") + + plt.show() + return ani + +if __name__ == "__main__": + print("=" * 60) + print("1D CFD 动画演示程序") + print("=" * 60) + print("\n选项:") + print("1. 创建完整动画(可能有问题)") + print("2. 创建静态关键帧图像") + print("3. 创建简单波动动画") + + try: + choice = int(input("\n请选择 (1-3, 默认=2): ") or "2") + except: + choice = 2 + + if choice == 1: + print("\n正在创建完整动画...") + print("注意:保存视频需要ffmpeg或pillow库") + print("如果失败,将自动回退到静态图像") + + # 先尝试创建静态图像 + create_static_frames() + + # 然后尝试动画 + try: + ani = create_cfd_animation(save_video=True, show_plot=True) + except Exception as e: + print(f"\n动画创建失败: {e}") + print("已创建静态图像作为替代。") + + elif choice == 2: + print("\n正在创建静态关键帧图像...") + create_static_frames() + + elif choice == 3: + print("\n正在创建简单波动动画...") + create_simple_animation() + + else: + print("\n无效选择,创建静态关键帧图像...") + create_static_frames() + + print("\n程序执行完成!") \ No newline at end of file diff --git a/example/figure/1d/mesh/01/05_interactive_cfd_plot.py b/example/figure/1d/mesh/01/05_interactive_cfd_plot.py new file mode 100644 index 00000000..6e034b89 --- /dev/null +++ b/example/figure/1d/mesh/01/05_interactive_cfd_plot.py @@ -0,0 +1,574 @@ +""" +文件名: 05_interactive_cfd_plot.py +功能: 创建交互式CFD可视化 +包含: 使用Plotly创建可交互的CFD图像 +注意: 需要安装plotly库: pip install plotly +""" + +import numpy as np +import plotly.graph_objects as go +from plotly.subplots import make_subplots +import plotly.io as pio + +def create_interactive_cfd_plot(): + """创建交互式CFD可视化""" + + print("正在创建交互式CFD可视化...") + print("这将在浏览器中打开一个交互式图表。") + + # 创建数据 + x = np.linspace(0, 10, 100) + + # 精确解和数值解 + u_exact = np.sin(x * 0.8) * np.exp(-0.1*x) + + # 添加不同数值格式的"数值解" + np.random.seed(42) + u_upwind = u_exact + 0.08*np.random.randn(len(x)) + u_central = u_exact + 0.05*np.sin(5*x)*0.3 # 模拟振荡 + u_quick = u_exact + 0.02*np.random.randn(len(x)) + + # 创建子图 + fig = make_subplots( + rows=2, cols=2, + subplot_titles=('Upwind Scheme (1st Order)', + 'Central Difference Scheme (2nd Order)', + 'QUICK Scheme (3rd Order)', + 'Grid Point Stencil Dependencies'), + vertical_spacing=0.12, + horizontal_spacing=0.1 + ) + + # 1. 迎风格式 + fig.add_trace( + go.Scatter( + x=x, y=u_exact, + mode='lines', + name='Exact Solution', + line=dict(color='black', width=3, dash='solid'), + showlegend=True, + legendgroup="exact" + ), + row=1, col=1 + ) + + fig.add_trace( + go.Scatter( + x=x, y=u_upwind, + mode='lines', + name='Upwind Scheme', + line=dict(color='red', width=2, dash='dash'), + showlegend=True, + legendgroup="upwind" + ), + row=1, col=1 + ) + + # 2. 中心差分格式 + fig.add_trace( + go.Scatter( + x=x, y=u_exact, + mode='lines', + name='Exact Solution', + line=dict(color='black', width=3, dash='solid'), + showlegend=False, + legendgroup="exact" + ), + row=1, col=2 + ) + + fig.add_trace( + go.Scatter( + x=x, y=u_central, + mode='lines', + name='Central Difference', + line=dict(color='blue', width=2, dash='dash'), + showlegend=True, + legendgroup="central" + ), + row=1, col=2 + ) + + # 3. QUICK格式 + fig.add_trace( + go.Scatter( + x=x, y=u_exact, + mode='lines', + name='Exact Solution', + line=dict(color='black', width=3, dash='solid'), + showlegend=False, + legendgroup="exact" + ), + row=2, col=1 + ) + + fig.add_trace( + go.Scatter( + x=x, y=u_quick, + mode='lines', + name='QUICK Scheme', + line=dict(color='green', width=2, dash='dash'), + showlegend=True, + legendgroup="quick" + ), + row=2, col=1 + ) + + # 4. 网格点模板依赖关系 + # 创建网格点 + grid_points = np.array([0, 1, 2, 3, 4]) + point_names = ['u_{i-2}', 'u_{i-1}', 'u_i', 'u_{i+1}', 'u_{i+2}'] + + # 添加网格点 + fig.add_trace( + go.Scatter( + x=grid_points, + y=[0, 0, 0, 0, 0], + mode='markers+text', + name='Grid Points', + marker=dict(size=15, color='gray'), + text=point_names, + textposition="top center", + showlegend=False + ), + row=2, col=2 + ) + + # 添加箭头表示依赖关系 + # 迎风格式箭头(从i-1到i) + fig.add_annotation( + x=1, y=0, + ax=2, ay=0, + xref="x4", yref="y4", + axref="x4", ayref="y4", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor="red", + row=2, col=2 + ) + + # 中心差分箭头(从i-1和i+1到i) + fig.add_annotation( + x=1, y=-0.05, + ax=2, ay=-0.05, + xref="x4", yref="y4", + axref="x4", ayref="y4", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor="blue", + row=2, col=2 + ) + + fig.add_annotation( + x=3, y=-0.05, + ax=2, ay=-0.05, + xref="x4", yref="y4", + axref="x4", ayref="y4", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor="blue", + row=2, col=2 + ) + + # QUICK格式箭头(从i-2, i-1, i+1到i) + fig.add_annotation( + x=0, y=0.05, + ax=2, ay=0.05, + xref="x4", yref="y4", + axref="x4", ayref="y4", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor="green", + row=2, col=2 + ) + + fig.add_annotation( + x=1, y=0.05, + ax=2, ay=0.05, + xref="x4", yref="y4", + axref="x4", ayref="y4", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor="green", + row=2, col=2 + ) + + fig.add_annotation( + x=3, y=0.05, + ax=2, ay=0.05, + xref="x4", yref="y4", + axref="x4", ayref="y4", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor="green", + row=2, col=2 + ) + + # 添加文本标签 + fig.add_annotation( + x=1.5, y=-0.15, + text="Upwind", + showarrow=False, + font=dict(color="red", size=12), + row=2, col=2 + ) + + fig.add_annotation( + x=2, y=-0.25, + text="Central", + showarrow=False, + font=dict(color="blue", size=12), + row=2, col=2 + ) + + fig.add_annotation( + x=2, y=0.2, + text="QUICK", + showarrow=False, + font=dict(color="green", size=12), + row=2, col=2 + ) + + # 更新布局 + fig.update_layout( + title_text="Interactive CFD Visualization: Comparison of Convection Schemes", + title_font_size=20, + title_x=0.5, + height=900, + width=1200, + showlegend=True, + legend=dict( + yanchor="top", + y=0.99, + xanchor="left", + x=1.02 + ) + ) + + # 更新轴标签 + fig.update_xaxes(title_text="Position x", row=1, col=1) + fig.update_yaxes(title_text="Velocity u", row=1, col=1) + + fig.update_xaxes(title_text="Position x", row=1, col=2) + fig.update_yaxes(title_text="Velocity u", row=1, col=2) + + fig.update_xaxes(title_text="Position x", row=2, col=1) + fig.update_yaxes(title_text="Velocity u", row=2, col=1) + + fig.update_xaxes(title_text="Grid Point Index", row=2, col=2, range=[-0.5, 4.5]) + fig.update_yaxes(title_text="", row=2, col=2, range=[-0.3, 0.3], showticklabels=False) + + # 保存为HTML文件 + html_filename = "05_interactive_cfd.html" + pio.write_html(fig, html_filename, auto_open=True) + + print(f"✓ 交互式图表已保存为 '{html_filename}'") + print("正在浏览器中打开...") + + return fig + +def create_interactive_grid_visualization(): + """创建交互式网格可视化""" + + print("\n创建交互式网格可视化...") + + # 创建网格数据 + n_cells = 5 + dx = 1.0 + x_vertices = np.linspace(0, n_cells*dx, n_cells + 1) + x_centers = (x_vertices[:-1] + x_vertices[1:]) / 2 + + # 创建子图 + fig = make_subplots( + rows=1, cols=2, + subplot_titles=('Vertex-centered Storage', 'Cell-centered Storage'), + horizontal_spacing=0.15 + ) + + # 1. 顶点中心存储 + # 添加顶点 + fig.add_trace( + go.Scatter( + x=x_vertices, + y=[0]*len(x_vertices), + mode='markers+text', + name='Vertices', + marker=dict(size=15, color='red', symbol='circle'), + text=[f'u{i}' for i in range(len(x_vertices))], + textposition="top center", + showlegend=False + ), + row=1, col=1 + ) + + # 添加控制体矩形(使用形状) + for i in range(n_cells): + fig.add_shape( + type="rect", + x0=x_vertices[i], + y0=-0.1, + x1=x_vertices[i+1], + y1=0.1, + line=dict(color="blue", width=2), + fillcolor="rgba(0, 0, 255, 0.1)", + row=1, col=1 + ) + + # 添加控制体标签 + fig.add_annotation( + x=(x_vertices[i] + x_vertices[i+1])/2, + y=0.15, + text=f"Cell {i}", + showarrow=False, + font=dict(size=10), + row=1, col=1 + ) + + # 2. 单元中心存储 + # 添加单元中心 + fig.add_trace( + go.Scatter( + x=x_centers, + y=[0]*len(x_centers), + mode='markers+text', + name='Cell Centers', + marker=dict(size=15, color='green', symbol='circle'), + text=[f'u{i}' for i in range(len(x_centers))], + textposition="top center", + showlegend=False + ), + row=1, col=2 + ) + + # 添加控制体矩形 + for i in range(n_cells): + fig.add_shape( + type="rect", + x0=x_vertices[i], + y0=-0.1, + x1=x_vertices[i+1], + y1=0.1, + line=dict(color="orange", width=2), + fillcolor="rgba(255, 165, 0, 0.1)", + row=1, col=2 + ) + + # 添加控制体标签 + fig.add_annotation( + x=(x_vertices[i] + x_vertices[i+1])/2, + y=0.15, + text=f"Cell {i}", + showarrow=False, + font=dict(size=10), + row=1, col=2 + ) + + # 标记边界 + fig.add_annotation( + x=x_vertices[0], + y=0.25, + text="Boundary", + showarrow=True, + arrowhead=2, + font=dict(color="red", size=12), + row=1, col=1 + ) + + fig.add_annotation( + x=x_vertices[-1], + y=0.25, + text="Boundary", + showarrow=True, + arrowhead=2, + font=dict(color="red", size=12), + row=1, col=1 + ) + + # 更新布局 + fig.update_layout( + title_text="Interactive CFD Grid Visualization", + title_font_size=20, + title_x=0.5, + height=500, + width=1000, + showlegend=False + ) + + # 更新轴 + for col in [1, 2]: + fig.update_xaxes(title_text="Position x", row=1, col=col, range=[-0.5, n_cells*dx+0.5]) + fig.update_yaxes(title_text="", row=1, col=col, range=[-0.3, 0.3], showticklabels=False) + + # 保存为HTML + html_filename = "05_interactive_grid.html" + pio.write_html(fig, html_filename, auto_open=True) + + print(f"✓ 网格可视化已保存为 '{html_filename}'") + + return fig + +def create_interactive_boundary_conditions(): + """创建交互式边界条件可视化""" + + print("\n创建交互式边界条件可视化...") + + n_cells = 5 + dx = 1.0 + + # 创建图形 + fig = go.Figure() + + # 真实计算点 + x_real = np.linspace(0, n_cells*dx, n_cells + 1) + + # 虚拟点 + x_ghost_left = [-dx] + x_ghost_right = [n_cells*dx + dx] + + # 添加点 + fig.add_trace(go.Scatter( + x=x_real, + y=[0]*len(x_real), + mode='markers+text', + name='Real Points', + marker=dict(size=15, color='blue', symbol='circle'), + text=[f'u{i}' for i in range(len(x_real))], + textposition="top center" + )) + + fig.add_trace(go.Scatter( + x=x_ghost_left + x_ghost_right, + y=[0, 0], + mode='markers+text', + name='Ghost Cells', + marker=dict(size=15, color='red', symbol='square'), + text=['Ghost L', 'Ghost R'], + textposition="top center" + )) + + # 添加区域 + fig.add_vrect( + x0=-dx, x1=0, + fillcolor="red", opacity=0.1, + line_width=0, + annotation_text="Ghost Cell Region", + annotation_position="top left" + ) + + fig.add_vrect( + x0=n_cells*dx, x1=n_cells*dx+dx, + fillcolor="red", opacity=0.1, + line_width=0, + annotation_text="Ghost Cell Region" + ) + + fig.add_vrect( + x0=0, x1=n_cells*dx, + fillcolor="green", opacity=0.1, + line_width=0, + annotation_text="Computational Domain", + annotation_position="top" + ) + + # 添加边界线 + fig.add_vline(x=0, line_width=3, line_dash="dash", line_color="red") + fig.add_vline(x=n_cells*dx, line_width=3, line_dash="dash", line_color="red") + + # 添加边界条件说明 + fig.add_annotation( + x=-dx/2, y=0.2, + text="Dirichlet BC:
u = u_boundary", + showarrow=False, + font=dict(size=11), + align="center" + ) + + fig.add_annotation( + x=n_cells*dx + dx/2, y=0.2, + text="Neumann BC:
∂u/∂x = 0", + showarrow=False, + font=dict(size=11), + align="center" + ) + + # 更新布局 + fig.update_layout( + title="Boundary Conditions and Ghost Cells", + xaxis_title="Position x", + yaxis_title="", + height=500, + width=800, + showlegend=True, + yaxis=dict(showticklabels=False, range=[-0.3, 0.4]) + ) + + # 保存为HTML + html_filename = "05_interactive_boundary.html" + pio.write_html(fig, html_filename, auto_open=True) + + print(f"✓ 边界条件可视化已保存为 '{html_filename}'") + + return fig + +def main(): + """主函数""" + print("=" * 60) + print("Interactive CFD Visualization with Plotly") + print("=" * 60) + print("\n选项:") + print("1. 对流格式比较") + print("2. 网格存储方式") + print("3. 边界条件处理") + print("4. 全部创建") + + try: + choice = input("\n请选择 (1-4, 默认=1): ").strip() + if choice == "": + choice = "1" + choice = int(choice) + except: + choice = 1 + + if choice == 1: + fig = create_interactive_cfd_plot() + elif choice == 2: + fig = create_interactive_grid_visualization() + elif choice == 3: + fig = create_interactive_boundary_conditions() + elif choice == 4: + print("\n创建所有交互式可视化...") + fig1 = create_interactive_cfd_plot() + fig2 = create_interactive_grid_visualization() + fig3 = create_interactive_boundary_conditions() + print("\n✓ 所有可视化已创建完成!") + else: + fig = create_interactive_cfd_plot() + + print("\n" + "=" * 60) + print("说明:") + print("- 交互式图表已保存为HTML文件") + print("- 可以在任何浏览器中打开查看") + print("- 支持缩放、平移、悬停查看数据点") + print("=" * 60) + +if __name__ == "__main__": + # 检查是否安装了plotly + try: + import plotly + main() + except ImportError: + print("错误: 需要安装plotly库") + print("请运行: pip install plotly") + print("或: pip install plotly numpy") \ No newline at end of file diff --git a/example/figure/1d/mesh/01/testprj.py b/example/figure/1d/mesh/01/testprj.py new file mode 100644 index 00000000..43af9550 --- /dev/null +++ b/example/figure/1d/mesh/01/testprj.py @@ -0,0 +1,140 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle +import matplotlib.gridspec as gridspec + +def plot_cfd_grid_storage(): + """绘制一维CFD网格和变量存储方式对比""" + fig = plt.figure(figsize=(14, 8)) + + # 创建网格 + n_cells = 5 + dx = 1.0 + x_vertices = np.linspace(0, n_cells*dx, n_cells + 1) + x_centers = (x_vertices[:-1] + x_vertices[1:]) / 2 + + # 1. 顶点中心存储 + ax1 = plt.subplot(2, 2, 1) + ax1.set_title("顶点中心存储 (Vertex-centered)", fontsize=12, fontweight='bold') + + # 绘制网格线 + for x in x_vertices: + ax1.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制顶点 + ax1.scatter(x_vertices, np.zeros_like(x_vertices), + s=100, color='red', zorder=5, label='变量存储点') + + # 标记顶点 + for i, x in enumerate(x_vertices): + ax1.text(x, -0.15, f'$u_{i}$', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="yellow", alpha=0.7)) + + # 绘制控制体 + for i in range(len(x_vertices)-1): + center = (x_vertices[i] + x_vertices[i+1]) / 2 + ax1.add_patch(Rectangle((x_vertices[i], -0.05), dx, 0.1, + alpha=0.2, color='blue', label='控制体' if i==0 else None)) + ax1.text(center, 0.1, f'Cell {i}', ha='center', fontsize=9) + + ax1.set_xlim(-0.5, n_cells*dx + 0.5) + ax1.set_ylim(-0.3, 0.3) + ax1.set_xlabel('x') + ax1.legend(loc='upper right') + ax1.grid(True, alpha=0.3) + + # 2. 单元中心存储 + ax2 = plt.subplot(2, 2, 2) + ax2.set_title("单元中心存储 (Cell-centered)", fontsize=12, fontweight='bold') + + # 绘制网格线 + for x in x_vertices: + ax2.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制单元中心 + ax2.scatter(x_centers, np.zeros_like(x_centers), + s=100, color='green', zorder=5, label='变量存储点') + + # 标记变量 + for i, x in enumerate(x_centers): + ax2.text(x, -0.15, f'$u_{i}$', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen", alpha=0.7)) + + # 绘制控制体 + for i, x in enumerate(x_vertices[:-1]): + ax2.add_patch(Rectangle((x, -0.05), dx, 0.1, + alpha=0.2, color='orange', label='控制体' if i==0 else None)) + ax2.text(x + dx/2, 0.1, f'Cell {i}', ha='center', fontsize=9) + + ax2.set_xlim(-0.5, n_cells*dx + 0.5) + ax2.set_ylim(-0.3, 0.3) + ax2.set_xlabel('x') + ax2.legend(loc='upper right') + ax2.grid(True, alpha=0.3) + + # 3. 边界条件处理示意 + ax3 = plt.subplot(2, 2, 3) + ax3.set_title("边界条件与虚拟点", fontsize=12, fontweight='bold') + + # 扩展网格显示虚拟点 + x_extended = np.linspace(-dx, (n_cells+1)*dx, n_cells + 4) + x_real = x_extended[1:-2] # 真实计算区域 + + # 绘制所有点 + ax3.scatter(x_extended, np.zeros_like(x_extended), s=50, color='gray', alpha=0.5) + ax3.scatter(x_real, np.zeros_like(x_real), s=100, color='blue', label='计算点') + + # 标记区域 + ax3.axvline(0, color='red', linestyle='--', linewidth=2, label='左边界') + ax3.axvline(n_cells*dx, color='red', linestyle='--', linewidth=2, label='右边界') + ax3.axvspan(-dx, 0, alpha=0.1, color='red', label='虚拟点区域') + ax3.axvspan(n_cells*dx, (n_cells+1)*dx, alpha=0.1, color='red') + + # 标记点类型 + ax3.text(-dx/2, 0.1, '虚拟点', ha='center', fontsize=9, + bbox=dict(boxstyle="round,pad=0.3", facecolor="pink", alpha=0.7)) + ax3.text(n_cells*dx + dx/2, 0.1, '虚拟点', ha='center', fontsize=9, + bbox=dict(boxstyle="round,pad=0.3", facecolor="pink", alpha=0.7)) + + ax3.set_xlim(-1.5*dx, (n_cells+1.5)*dx) + ax3.set_ylim(-0.2, 0.3) + ax3.set_xlabel('x') + ax3.legend(loc='upper right') + ax3.grid(True, alpha=0.3) + + # 4. 数值格式示意 + ax4 = plt.subplot(2, 2, 4) + ax4.set_title("有限差分格式示意", fontsize=12, fontweight='bold') + + # 创建示例数据 + x = np.linspace(0, 10, 50) + u = np.sin(x * 0.8) * np.exp(-0.1*x) + + # 选取几个点 + i = 25 + ax4.plot(x, u, 'b-', linewidth=2, label='真实解') + ax4.scatter(x[i], u[i], s=150, color='red', zorder=5, label='计算点 $u_i$') + ax4.scatter(x[i-1], u[i-1], s=100, color='green', zorder=4, label='$u_{i-1}$') + ax4.scatter(x[i+1], u[i+1], s=100, color='orange', zorder=4, label='$u_{i+1}$') + + # 绘制差分示意 + ax4.plot([x[i-1], x[i+1]], [u[i-1], u[i+1]], 'k--', alpha=0.5) + ax4.annotate('中心差分', xy=(x[i], u[i]), xytext=(x[i], u[i]+0.3), + arrowprops=dict(arrowstyle="->", color='black'), + ha='center', fontsize=10) + + ax4.annotate('迎风格式\n使用上游点', xy=(x[i], u[i]), xytext=(x[i]-2, u[i]-0.3), + arrowprops=dict(arrowstyle="->", color='red'), + ha='center', fontsize=9) + + ax4.set_xlabel('x') + ax4.set_ylabel('u') + ax4.legend(loc='upper right') + ax4.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig('cfd_grid_illustration.png', dpi=300, bbox_inches='tight') + plt.show() + +# 运行绘图 +plot_cfd_grid_storage() \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/00_testanimation.py b/example/figure/1d/mesh/01a/00_testanimation.py new file mode 100644 index 00000000..53ae992e --- /dev/null +++ b/example/figure/1d/mesh/01a/00_testanimation.py @@ -0,0 +1,28 @@ +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import numpy as np + +# 创建图形和轴 +fig, ax = plt.subplots() +ax.set_xlim(0, 2*np.pi) +ax.set_ylim(-1, 1) + +# 初始化数据 +x = np.linspace(0, 2*np.pi, 1000) +line, = ax.plot(x, np.sin(x), color='blue') + +# 动画更新函数 +def animate(frame): + # 随着帧数增加,x 数据偏移,实现波形移动 + y = np.sin(x + frame / 10.0) + line.set_ydata(y) + return line, + +# 创建动画:100 帧,每帧间隔 20ms,支持 blit 优化(更快渲染) +ani = animation.FuncAnimation(fig, animate, frames=100, interval=20, blit=True) + +# 显示动画(在 Jupyter 中可用 plt.show(),否则保存为 GIF) +plt.show() + +# 可选:保存为 GIF 文件(需要 pillow 或 imagemagick) +# ani.save('sine_wave.gif', writer='pillow', fps=30) \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/01_cfd_grid_storage.py b/example/figure/1d/mesh/01a/01_cfd_grid_storage.py new file mode 100644 index 00000000..9f686ecc --- /dev/null +++ b/example/figure/1d/mesh/01a/01_cfd_grid_storage.py @@ -0,0 +1,214 @@ +""" +文件名: 01_cfd_grid_storage.py +功能: 绘制一维CFD基础网格与变量存储示意图 +包含: 顶点中心存储、单元中心存储、边界条件处理、有限差分格式示意 +""" + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14, + 'figure.dpi': 100 + }) + +def plot_cfd_grid_storage(): + """绘制一维CFD网格和变量存储方式对比""" + setup_plot_style() + fig = plt.figure(figsize=(15, 10)) + + # 创建网格 + n_cells = 5 + dx = 1.0 + x_vertices = np.linspace(0, n_cells*dx, n_cells + 1) + x_centers = (x_vertices[:-1] + x_vertices[1:]) / 2 + + # 1. 顶点中心存储 + ax1 = plt.subplot(2, 2, 1) + ax1.set_title("Vertex-centered Storage", fontsize=14, fontweight='bold', pad=20) + + # 绘制网格线 + for x in x_vertices: + ax1.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制顶点 + ax1.scatter(x_vertices, np.zeros_like(x_vertices), + s=120, color='red', zorder=5, edgecolors='black', linewidth=1.5) + + # 标记顶点 + for i, x in enumerate(x_vertices): + if i == 0: + label = f'Boundary\n$u_0$' + color = "orange" + elif i == len(x_vertices) - 1: + label = f'Boundary\n$u_{i}$' + color = "orange" + else: + label = f'Storage\n$u_{i}$' + color = "yellow" + + ax1.text(x, -0.2, label, ha='center', fontsize=10, va='top', + bbox=dict(boxstyle="round,pad=0.4", facecolor=color, alpha=0.8, edgecolor='black')) + + # 绘制控制体 + for i in range(len(x_vertices)-1): + center = (x_vertices[i] + x_vertices[i+1]) / 2 + rect = ax1.add_patch(Rectangle((x_vertices[i], -0.08), dx, 0.16, + alpha=0.2, color='blue', + edgecolor='blue', linewidth=1.5)) + ax1.text(center, 0.15, f'Cell {i}', ha='center', fontsize=11, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue", alpha=0.8)) + + ax1.set_xlim(-0.5, n_cells*dx + 0.5) + ax1.set_ylim(-0.35, 0.35) + ax1.set_xlabel('Position x') + ax1.set_ylabel('Variable Storage') + ax1.text(0.5, 0.95, 'Variables stored at vertices', transform=ax1.transAxes, + ha='center', fontsize=11, bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow")) + ax1.grid(True, alpha=0.3) + + # 2. 单元中心存储 + ax2 = plt.subplot(2, 2, 2) + ax2.set_title("Cell-centered Storage", fontsize=14, fontweight='bold', pad=20) + + # 绘制网格线 + for x in x_vertices: + ax2.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制单元中心 + ax2.scatter(x_centers, np.zeros_like(x_centers), + s=120, color='green', zorder=5, edgecolors='black', linewidth=1.5) + + # 标记变量 + for i, x in enumerate(x_centers): + label = f'Storage\n$u_{i}$' + ax2.text(x, -0.2, label, ha='center', fontsize=10, va='top', + bbox=dict(boxstyle="round,pad=0.4", facecolor="lightgreen", alpha=0.8, edgecolor='black')) + + # 绘制控制体 + for i, x in enumerate(x_vertices[:-1]): + rect = ax2.add_patch(Rectangle((x, -0.08), dx, 0.16, + alpha=0.2, color='orange', + edgecolor='orange', linewidth=1.5)) + ax2.text(x + dx/2, 0.15, f'Cell {i}', ha='center', fontsize=11, + bbox=dict(boxstyle="round,pad=0.3", facecolor="peachpuff", alpha=0.8)) + + # 标记边界 + ax2.axvline(x_vertices[0], color='red', linestyle='--', linewidth=2, alpha=0.7) + ax2.axvline(x_vertices[-1], color='red', linestyle='--', linewidth=2, alpha=0.7) + ax2.text(x_vertices[0], 0.25, 'Boundary', ha='center', color='red', fontsize=11, fontweight='bold') + ax2.text(x_vertices[-1], 0.25, 'Boundary', ha='center', color='red', fontsize=11, fontweight='bold') + + ax2.set_xlim(-0.5, n_cells*dx + 0.5) + ax2.set_ylim(-0.35, 0.35) + ax2.set_xlabel('Position x') + ax2.set_ylabel('Variable Storage') + ax2.text(0.5, 0.95, 'Variables stored at cell centers', transform=ax2.transAxes, + ha='center', fontsize=11, bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow")) + ax2.grid(True, alpha=0.3) + + # 3. 边界条件处理示意 + ax3 = plt.subplot(2, 2, 3) + ax3.set_title("Boundary Conditions and Ghost Cells", fontsize=14, fontweight='bold', pad=20) + + # 扩展网格显示虚拟点 + x_extended = np.linspace(-dx, (n_cells+1)*dx, n_cells + 4) + x_real = x_extended[1:-2] # 真实计算区域 + + # 绘制所有点 + ax3.scatter(x_extended, np.zeros_like(x_extended), s=80, color='gray', alpha=0.5) + ax3.scatter(x_real, np.zeros_like(x_real), s=120, color='blue', + edgecolors='black', linewidth=1.5, zorder=5) + + # 标记边界 + ax3.axvline(0, color='red', linestyle='-', linewidth=3, alpha=0.8) + ax3.axvline(n_cells*dx, color='red', linestyle='-', linewidth=3, alpha=0.8) + + # 填充虚拟点区域 + ax3.axvspan(-dx, 0, alpha=0.15, color='red', hatch='//') + ax3.axvspan(n_cells*dx, (n_cells+1)*dx, alpha=0.15, color='red', hatch='//') + + # 标记点类型 + ax3.text(-dx/2, 0.15, 'Ghost Cell', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.4", facecolor="pink", alpha=0.9, edgecolor='red')) + ax3.text(n_cells*dx + dx/2, 0.15, 'Ghost Cell', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.4", facecolor="pink", alpha=0.9, edgecolor='red')) + + # 标记计算区域 + ax3.axvspan(0, n_cells*dx, alpha=0.1, color='green') + ax3.text(n_cells*dx/2, -0.2, 'Computational Domain', ha='center', fontsize=12, + bbox=dict(boxstyle="round,pad=0.4", facecolor="lightgreen", alpha=0.8)) + + # 添加箭头示意边界条件 + ax3.annotate('BC: u=0', xy=(0, 0), xytext=(-1.2*dx, 0.25), + arrowprops=dict(arrowstyle="->", color='darkred', lw=2), + ha='center', fontsize=10, color='darkred', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax3.annotate('BC: ∂u/∂x=0', xy=(n_cells*dx, 0), + xytext=(n_cells*dx + 1.2*dx, 0.25), + arrowprops=dict(arrowstyle="->", color='darkred', lw=2), + ha='center', fontsize=10, color='darkred', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax3.set_xlim(-1.8*dx, (n_cells+1.8)*dx) + ax3.set_ylim(-0.3, 0.4) + ax3.set_xlabel('Position x') + ax3.set_ylabel('Domain') + ax3.grid(True, alpha=0.3) + + # 4. 数值格式示意 + ax4 = plt.subplot(2, 2, 4) + ax4.set_title("Finite Difference Schemes", fontsize=14, fontweight='bold', pad=20) + + # 创建示例数据 + x = np.linspace(0, 10, 50) + u = np.sin(x * 0.8) * np.exp(-0.1*x) + + # 选取几个点 + i = 25 + ax4.plot(x, u, 'b-', linewidth=3, alpha=0.5, label='Exact Solution') + ax4.scatter(x[i], u[i], s=200, color='red', zorder=5, + edgecolors='black', linewidth=1.5, label='Current point $u_i$') + ax4.scatter(x[i-1], u[i-1], s=150, color='green', zorder=4, + edgecolors='black', linewidth=1, label='Upstream $u_{i-1}$') + ax4.scatter(x[i+1], u[i+1], s=150, color='orange', zorder=4, + edgecolors='black', linewidth=1, label='Downstream $u_{i+1}$') + + # 绘制差分示意线 + ax4.plot([x[i-1], x[i+1]], [u[i-1], u[i+1]], 'k--', alpha=0.5, linewidth=1.5) + + # 标注差分格式 + ax4.annotate('Central Difference\n(2nd order)', xy=(x[i], u[i]), xytext=(x[i], u[i]+0.4), + arrowprops=dict(arrowstyle="->", color='blue', lw=2), + ha='center', fontsize=11, color='blue', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax4.annotate('Upwind Scheme\n(1st order)', xy=(x[i], u[i]), xytext=(x[i]-2.5, u[i]-0.3), + arrowprops=dict(arrowstyle="->", color='red', lw=2), + ha='center', fontsize=11, color='red', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax4.set_xlabel('Position x') + ax4.set_ylabel('Variable u') + ax4.legend(loc='upper right', fontsize=10) + ax4.grid(True, alpha=0.3) + + # 添加整体标题 + plt.suptitle('1D CFD Grid and Variable Storage Illustration', fontsize=16, fontweight='bold', y=1.02) + + plt.tight_layout() + plt.savefig('01_cfd_grid_storage.png', dpi=300, bbox_inches='tight', facecolor='white') + plt.show() + +if __name__ == "__main__": + plot_cfd_grid_storage() \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/02_convection_schemes.py b/example/figure/1d/mesh/01a/02_convection_schemes.py new file mode 100644 index 00000000..a2b2c413 --- /dev/null +++ b/example/figure/1d/mesh/01a/02_convection_schemes.py @@ -0,0 +1,126 @@ +""" +文件名: 02_convection_schemes.py +功能: 绘制一维对流方程不同数值格式对比 +包含: 迎风格式、中心差分格式、QUICK格式的比较 +""" + +import numpy as np +import matplotlib.pyplot as plt + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14 + }) + +def plot_convection_schemes(): + """绘制不同对流格式示意图""" + setup_plot_style() + + # 创建数据 + x = np.linspace(0, 2*np.pi, 100) + u_exact = np.sin(x) + + # 添加数值噪声模拟数值解 + np.random.seed(42) + u_upwind = u_exact + 0.1*np.random.randn(len(x)) # 迎风格式(有耗散) + u_central = u_exact + 0.05*np.random.randn(len(x)) # 中心差分(有振荡) + u_quick = u_exact + 0.02*np.random.randn(len(x)) # QUICK格式 + + fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + + # 1. 迎风格式 + ax = axes[0, 0] + ax.plot(x, u_exact, 'k-', linewidth=3, alpha=0.7, label='Exact Solution') + ax.plot(x, u_upwind, 'r--', linewidth=2, marker='o', markersize=4, + markevery=5, label='Upwind Scheme') + ax.fill_between(x, u_exact-0.15, u_exact+0.15, alpha=0.1, color='gray') + ax.set_title('Upwind Scheme', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + ax.annotate('Numerical\nDissipation', xy=(3, 0), xytext=(4, -0.8), + arrowprops=dict(arrowstyle="->", color='red'), + fontsize=10, color='red') + + # 2. 中心差分格式 + ax = axes[0, 1] + ax.plot(x, u_exact, 'k-', linewidth=3, alpha=0.7, label='Exact Solution') + ax.plot(x, u_central, 'b--', linewidth=2, marker='s', markersize=4, + markevery=5, label='Central Difference') + ax.set_title('Central Difference Scheme', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + ax.annotate('Numerical\nOscillation', xy=(1.5, 0.5), xytext=(0.5, 0.8), + arrowprops=dict(arrowstyle="->", color='blue'), + fontsize=10, color='blue') + + # 3. QUICK格式 + ax = axes[1, 0] + ax.plot(x, u_exact, 'k-', linewidth=3, alpha=0.7, label='Exact Solution') + ax.plot(x, u_quick, 'g--', linewidth=2, marker='^', markersize=4, + markevery=5, label='QUICK Scheme') + ax.set_title('QUICK Scheme (3rd Order)', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 4. 格式示意 + ax = axes[1, 1] + ax.set_title('Grid Point Dependencies', fontsize=12, fontweight='bold') + + # 绘制网格点 + points_x = np.array([0, 1, 2, 3, 4]) + points_y = np.zeros_like(points_x) + + ax.scatter(points_x, points_y, s=200, color='gray') + + # 标记点 + labels = ['$u_{i-2}$', '$u_{i-1}$', '$u_i$', '$u_{i+1}$', '$u_{i+2}$'] + for i, (x_pos, label) in enumerate(zip(points_x, labels)): + ax.text(x_pos, 0.1, label, ha='center', fontsize=12, fontweight='bold') + + # 迎风格式依赖 + ax.annotate('', xy=(2, 0), xytext=(1, 0), + arrowprops=dict(arrowstyle='<-', color='red', lw=2)) + ax.text(1.5, -0.15, 'Upwind', ha='center', color='red', fontweight='bold') + + # 中心差分依赖 + ax.annotate('', xy=(2, 0), xytext=(1, -0.05), + arrowprops=dict(arrowstyle='<-', color='blue', lw=2)) + ax.annotate('', xy=(2, 0), xytext=(3, -0.05), + arrowprops=dict(arrowstyle='<-', color='blue', lw=2)) + ax.text(2, -0.2, 'Central', ha='center', color='blue', fontweight='bold') + + # QUICK格式依赖 + ax.annotate('', xy=(2, 0), xytext=(0, 0.05), + arrowprops=dict(arrowstyle='<-', color='green', lw=2, alpha=0.7)) + ax.annotate('', xy=(2, 0), xytext=(1, 0.05), + arrowprops=dict(arrowstyle='<-', color='green', lw=2, alpha=0.7)) + ax.annotate('', xy=(2, 0), xytext=(3, 0.05), + arrowprops=dict(arrowstyle='<-', color='green', lw=2, alpha=0.7)) + ax.text(2, 0.2, 'QUICK', ha='center', color='green', fontweight='bold') + + ax.set_xlim(-0.5, 4.5) + ax.set_ylim(-0.3, 0.3) + ax.set_xlabel('Grid Point Index') + ax.grid(True, alpha=0.3) + ax.set_yticks([]) + + plt.suptitle('Comparison of Convection Schemes for 1D CFD', fontsize=14, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('02_convection_schemes.png', dpi=300, bbox_inches='tight') + plt.show() + +if __name__ == "__main__": + plot_convection_schemes() \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/03_interpolation_methods.py b/example/figure/1d/mesh/01a/03_interpolation_methods.py new file mode 100644 index 00000000..44eaf808 --- /dev/null +++ b/example/figure/1d/mesh/01a/03_interpolation_methods.py @@ -0,0 +1,119 @@ +""" +文件名: 03_interpolation_methods.py +功能: 绘制不同插值方法对比 +包含: 线性插值、二次插值、三次样条插值的比较 +""" + +import numpy as np +import matplotlib.pyplot as plt +from scipy import interpolate + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14 + }) + +def plot_interpolation_methods(): + """绘制不同插值方法对比""" + setup_plot_style() + + # 创建粗网格和细网格 + x_coarse = np.linspace(0, 10, 6) + u_coarse = np.sin(x_coarse * 0.8) + + x_fine = np.linspace(0, 10, 100) + u_exact = np.sin(x_fine * 0.8) + + # 不同插值方法 + # 线性插值 + f_linear = interpolate.interp1d(x_coarse, u_coarse, kind='linear') + u_linear = f_linear(x_fine) + + # 二次插值 + f_quadratic = interpolate.interp1d(x_coarse, u_coarse, kind='quadratic') + u_quadratic = f_quadratic(x_fine) + + # 三次样条插值 + f_cubic = interpolate.CubicSpline(x_coarse, u_coarse) + u_cubic = f_cubic(x_fine) + + fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + + # 1. 线性插值 + ax = axes[0, 0] + ax.plot(x_fine, u_exact, 'k-', alpha=0.3, linewidth=3, label='Exact Solution') + ax.plot(x_fine, u_linear, 'r--', linewidth=2, label='Linear Interpolation') + ax.scatter(x_coarse, u_coarse, s=100, color='blue', zorder=5, label='Known Points') + ax.set_title('Linear Interpolation', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 2. 二次插值 + ax = axes[0, 1] + ax.plot(x_fine, u_exact, 'k-', alpha=0.3, linewidth=3, label='Exact Solution') + ax.plot(x_fine, u_quadratic, 'g--', linewidth=2, label='Quadratic Interpolation') + ax.scatter(x_coarse, u_coarse, s=100, color='blue', zorder=5, label='Known Points') + ax.set_title('Quadratic Interpolation', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 3. 三次样条插值 + ax = axes[1, 0] + ax.plot(x_fine, u_exact, 'k-', alpha=0.3, linewidth=3, label='Exact Solution') + ax.plot(x_fine, u_cubic, 'b--', linewidth=2, label='Cubic Spline') + ax.scatter(x_coarse, u_coarse, s=100, color='blue', zorder=5, label='Known Points') + ax.set_title('Cubic Spline Interpolation', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 4. 误差比较 + ax = axes[1, 1] + errors = { + 'Linear': np.abs(u_linear - u_exact), + 'Quadratic': np.abs(u_quadratic - u_exact), + 'Cubic Spline': np.abs(u_cubic - u_exact) + } + + x_pos = np.arange(len(errors)) + mean_errors = [np.mean(err) for err in errors.values()] + max_errors = [np.max(err) for err in errors.values()] + + width = 0.35 + ax.bar(x_pos - width/2, mean_errors, width, label='Mean Error', color='skyblue') + ax.bar(x_pos + width/2, max_errors, width, label='Max Error', color='salmon') + + ax.set_xlabel('Interpolation Method') + ax.set_ylabel('Error') + ax.set_title('Interpolation Error Comparison', fontsize=12, fontweight='bold') + ax.set_xticks(x_pos) + ax.set_xticklabels(list(errors.keys())) + ax.legend() + ax.grid(True, alpha=0.3, axis='y') + + # 添加数值标签 + for i, (mean_err, max_err) in enumerate(zip(mean_errors, max_errors)): + ax.text(i - width/2, mean_err + 0.001, f'{mean_err:.3f}', + ha='center', va='bottom', fontsize=9) + ax.text(i + width/2, max_err + 0.001, f'{max_err:.3f}', + ha='center', va='bottom', fontsize=9) + + plt.suptitle('Comparison of Interpolation Methods for CFD', fontsize=14, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('03_interpolation_methods.png', dpi=300, bbox_inches='tight') + plt.show() + +if __name__ == "__main__": + plot_interpolation_methods() \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/04_cfd_animation.py b/example/figure/1d/mesh/01a/04_cfd_animation.py new file mode 100644 index 00000000..7315bdbc --- /dev/null +++ b/example/figure/1d/mesh/01a/04_cfd_animation.py @@ -0,0 +1,178 @@ +""" +文件名: 04_cfd_animation_fixed.py +功能: 完全按照简单模式修复的CFD动画 +参考: 你的成功示例代码 +""" + +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import numpy as np + +def create_simple_animation(): + """创建简单的CFD动画(只显示)""" + print("正在创建CFD动画...") + + # 创建图形和轴 + fig, ax = plt.subplots(figsize=(10, 6)) + ax.set_xlim(0, 10) + ax.set_ylim(-1.5, 1.5) + ax.set_xlabel('Position x') + ax.set_ylabel('Velocity u') + ax.grid(True, alpha=0.3) + + # 初始化数据 + x = np.linspace(0, 10, 200) + line_exact, = ax.plot(x, np.zeros_like(x), 'b-', linewidth=2, label='Exact Solution') + line_num, = ax.plot(x, np.zeros_like(x), 'r--', linewidth=2, label='Numerical Solution') + ax.legend() + + # 添加标题 + ax.set_title('1D Convection Equation Simulation') + + # 动画更新函数 + def animate(frame): + t = frame * 0.05 # 时间步长 + + # 精确解:波动传播 + u_exact = np.sin(2 * np.pi * (x/5 - t/2)) * np.exp(-0.1*x) + + # 数值解:添加一些数值误差(模拟迎风格式的耗散) + u_num = u_exact * (1 - 0.05*t) + 0.1*np.sin(5*x)*np.sin(t) + + line_exact.set_ydata(u_exact) + line_num.set_ydata(u_num) + ax.set_title(f'1D Convection Equation - Time: {t:.2f}s') + + return line_exact, line_num, + + # 创建动画:100帧,每帧间隔50ms + ani = animation.FuncAnimation(fig, animate, frames=100, interval=50, blit=True) + + print("动画准备就绪,显示窗口...") + print("提示:关闭窗口以退出程序。") + + plt.tight_layout() + plt.show() + + return ani + +def create_and_save_animation(): + """创建并保存动画(先保存再显示分开处理)""" + print("正在创建并保存CFD动画...") + + # ========== 第一部分:只保存动画 ========== + print("\n1. 保存动画到文件...") + + fig1, ax1 = plt.subplots(figsize=(10, 6)) + ax1.set_xlim(0, 10) + ax1.set_ylim(-1.5, 1.5) + + x = np.linspace(0, 10, 200) + line1, = ax1.plot(x, np.zeros_like(x), 'b-', lw=2) + line2, = ax1.plot(x, np.zeros_like(x), 'r--', lw=2) + + def update_save(frame): + t = frame * 0.05 + u_exact = np.sin(2 * np.pi * (x/5 - t/2)) + u_num = u_exact * (1 - 0.05*t) + + line1.set_ydata(u_exact) + line2.set_ydata(u_num) + ax1.set_title(f'Time: {t:.2f}s') + + return line1, line2, + + ani_save = animation.FuncAnimation(fig1, update_save, frames=50, blit=True) + + # 保存动画 + try: + ani_save.save('04_cfd_save_only.gif', writer='pillow', fps=15, dpi=100) + print("✓ 动画已保存为 '04_cfd_save_only.gif'") + except Exception as e: + print(f"保存失败: {e}") + + # 重要:关闭保存用的图形 + plt.close(fig1) + + # ========== 第二部分:只显示动画 ========== + print("\n2. 显示动画...") + create_simple_animation() + +def create_static_images(): + """创建静态图像""" + print("正在创建静态图像...") + + x = np.linspace(0, 10, 200) + times = [0, 0.5, 1.0, 1.5, 2.0] + + fig, axes = plt.subplots(2, 3, figsize=(15, 8)) + axes = axes.flatten() + + for i, (ax, t) in enumerate(zip(axes[:5], times)): + u_exact = np.sin(2 * np.pi * (x/5 - t/2)) + u_num = u_exact * (1 - 0.05*t) + + ax.plot(x, u_exact, 'b-', lw=2, label='Exact') + ax.plot(x, u_num, 'r--', lw=2, label='Numerical') + ax.set_xlim(0, 10) + ax.set_ylim(-1.5, 1.5) + ax.set_xlabel('Position x') + ax.set_ylabel('Velocity u') + ax.set_title(f'Time = {t:.1f}s') + ax.legend() + ax.grid(True, alpha=0.3) + + # 最后一个子图显示误差 + ax = axes[5] + t_vals = np.linspace(0, 2, 50) + errors = [] + + for t in t_vals: + u_exact = np.sin(2 * np.pi * (x/5 - t/2)) + u_num = u_exact * (1 - 0.05*t) + error = np.max(np.abs(u_exact - u_num)) + errors.append(error) + + ax.plot(t_vals, errors, 'k-', lw=2) + ax.fill_between(t_vals, 0, errors, alpha=0.3) + ax.set_xlabel('Time (s)') + ax.set_ylabel('Max Error') + ax.set_title('Numerical Error over Time') + ax.grid(True, alpha=0.3) + + plt.suptitle('1D Convection Equation - Static Frames', fontsize=16, y=1.02) + plt.tight_layout() + plt.savefig('04_cfd_static.png', dpi=300, bbox_inches='tight') + plt.show() + + print("✓ 静态图像已保存为 '04_cfd_static.png'") + +def main_menu(): + """主菜单""" + print("=" * 60) + print("CFD 动画演示程序") + print("=" * 60) + print("\n选项:") + print("1. 显示动画(简单,不会出错)") + print("2. 保存并显示动画(分开处理)") + print("3. 创建静态图像") + + try: + choice = input("\n请选择 (1-3, 默认=1): ").strip() + if choice == "": + choice = "1" + choice = int(choice) + except: + choice = 1 + + if choice == 1: + create_simple_animation() + elif choice == 2: + create_and_save_animation() + elif choice == 3: + create_static_images() + else: + create_simple_animation() + +if __name__ == "__main__": + main_menu() \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/05_interactive_cfd_plot.py b/example/figure/1d/mesh/01a/05_interactive_cfd_plot.py new file mode 100644 index 00000000..eadffe9b --- /dev/null +++ b/example/figure/1d/mesh/01a/05_interactive_cfd_plot.py @@ -0,0 +1,83 @@ +""" +文件名: 05_interactive_cfd_plot.py +功能: 创建交互式CFD可视化 +包含: 使用Plotly创建可交互的CFD图像 +注意: 需要安装plotly库: pip install plotly +""" + +import numpy as np +import plotly.graph_objects as go +from plotly.subplots import make_subplots + +def create_interactive_plot(): + """创建交互式CFD可视化""" + + # 创建数据 + x = np.linspace(0, 10, 100) + + # 精确解和数值解 + u_exact = np.sin(x * 0.8) * np.exp(-0.1*x) + + # 添加不同数值格式的"数值解" + np.random.seed(42) + u_upwind = u_exact + 0.08*np.random.randn(len(x)) + u_central = u_exact + 0.05*np.sin(5*x)*0.3 # 模拟振荡 + u_quick = u_exact + 0.02*np.random.randn(len(x)) + + # 创建子图 + fig = make_subplots( + rows=2, cols=2, + subplot_titles=('Upwind Scheme', + 'Central Difference Scheme', + 'QUICK Scheme', + 'Grid Point Stencil'), + vertical_spacing=0.12, + horizontal_spacing=0.1 + ) + + # 1. 迎风格式 + fig.add_trace( + go.Scatter( + x=x, y=u_exact, + mode='lines', + name='Exact Solution', + line=dict(color='black', width=3, dash='solid'), + showlegend=True + ), + row=1, col=1 + ) + + fig.add_trace( + go.Scatter( + x=x, y=u_upwind, + mode='lines+markers', + name='Upwind', + line=dict(color='red', width=2, dash='dash'), + marker=dict(size=4, symbol='circle'), + showlegend=True + ), + row=1, col=1 + ) + + # 2. 中心差分格式 + fig.add_trace( + go.Scatter( + x=x, y=u_exact, + mode='lines', + name='Exact Solution', + line=dict(color='black', width=3, dash='solid'), + showlegend=False + ), + row=1, col=2 + ) + + fig.add_trace( + go.Scatter( + x=x, y=u_central, + mode='lines+markers', + name='Central', + line=dict(color='blue', width=2, dash='dash'), + marker=dict(size=4, symbol='square'), + showlegend=True + ), + row=1 \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/testprj.py b/example/figure/1d/mesh/01a/testprj.py new file mode 100644 index 00000000..43af9550 --- /dev/null +++ b/example/figure/1d/mesh/01a/testprj.py @@ -0,0 +1,140 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle +import matplotlib.gridspec as gridspec + +def plot_cfd_grid_storage(): + """绘制一维CFD网格和变量存储方式对比""" + fig = plt.figure(figsize=(14, 8)) + + # 创建网格 + n_cells = 5 + dx = 1.0 + x_vertices = np.linspace(0, n_cells*dx, n_cells + 1) + x_centers = (x_vertices[:-1] + x_vertices[1:]) / 2 + + # 1. 顶点中心存储 + ax1 = plt.subplot(2, 2, 1) + ax1.set_title("顶点中心存储 (Vertex-centered)", fontsize=12, fontweight='bold') + + # 绘制网格线 + for x in x_vertices: + ax1.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制顶点 + ax1.scatter(x_vertices, np.zeros_like(x_vertices), + s=100, color='red', zorder=5, label='变量存储点') + + # 标记顶点 + for i, x in enumerate(x_vertices): + ax1.text(x, -0.15, f'$u_{i}$', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="yellow", alpha=0.7)) + + # 绘制控制体 + for i in range(len(x_vertices)-1): + center = (x_vertices[i] + x_vertices[i+1]) / 2 + ax1.add_patch(Rectangle((x_vertices[i], -0.05), dx, 0.1, + alpha=0.2, color='blue', label='控制体' if i==0 else None)) + ax1.text(center, 0.1, f'Cell {i}', ha='center', fontsize=9) + + ax1.set_xlim(-0.5, n_cells*dx + 0.5) + ax1.set_ylim(-0.3, 0.3) + ax1.set_xlabel('x') + ax1.legend(loc='upper right') + ax1.grid(True, alpha=0.3) + + # 2. 单元中心存储 + ax2 = plt.subplot(2, 2, 2) + ax2.set_title("单元中心存储 (Cell-centered)", fontsize=12, fontweight='bold') + + # 绘制网格线 + for x in x_vertices: + ax2.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制单元中心 + ax2.scatter(x_centers, np.zeros_like(x_centers), + s=100, color='green', zorder=5, label='变量存储点') + + # 标记变量 + for i, x in enumerate(x_centers): + ax2.text(x, -0.15, f'$u_{i}$', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen", alpha=0.7)) + + # 绘制控制体 + for i, x in enumerate(x_vertices[:-1]): + ax2.add_patch(Rectangle((x, -0.05), dx, 0.1, + alpha=0.2, color='orange', label='控制体' if i==0 else None)) + ax2.text(x + dx/2, 0.1, f'Cell {i}', ha='center', fontsize=9) + + ax2.set_xlim(-0.5, n_cells*dx + 0.5) + ax2.set_ylim(-0.3, 0.3) + ax2.set_xlabel('x') + ax2.legend(loc='upper right') + ax2.grid(True, alpha=0.3) + + # 3. 边界条件处理示意 + ax3 = plt.subplot(2, 2, 3) + ax3.set_title("边界条件与虚拟点", fontsize=12, fontweight='bold') + + # 扩展网格显示虚拟点 + x_extended = np.linspace(-dx, (n_cells+1)*dx, n_cells + 4) + x_real = x_extended[1:-2] # 真实计算区域 + + # 绘制所有点 + ax3.scatter(x_extended, np.zeros_like(x_extended), s=50, color='gray', alpha=0.5) + ax3.scatter(x_real, np.zeros_like(x_real), s=100, color='blue', label='计算点') + + # 标记区域 + ax3.axvline(0, color='red', linestyle='--', linewidth=2, label='左边界') + ax3.axvline(n_cells*dx, color='red', linestyle='--', linewidth=2, label='右边界') + ax3.axvspan(-dx, 0, alpha=0.1, color='red', label='虚拟点区域') + ax3.axvspan(n_cells*dx, (n_cells+1)*dx, alpha=0.1, color='red') + + # 标记点类型 + ax3.text(-dx/2, 0.1, '虚拟点', ha='center', fontsize=9, + bbox=dict(boxstyle="round,pad=0.3", facecolor="pink", alpha=0.7)) + ax3.text(n_cells*dx + dx/2, 0.1, '虚拟点', ha='center', fontsize=9, + bbox=dict(boxstyle="round,pad=0.3", facecolor="pink", alpha=0.7)) + + ax3.set_xlim(-1.5*dx, (n_cells+1.5)*dx) + ax3.set_ylim(-0.2, 0.3) + ax3.set_xlabel('x') + ax3.legend(loc='upper right') + ax3.grid(True, alpha=0.3) + + # 4. 数值格式示意 + ax4 = plt.subplot(2, 2, 4) + ax4.set_title("有限差分格式示意", fontsize=12, fontweight='bold') + + # 创建示例数据 + x = np.linspace(0, 10, 50) + u = np.sin(x * 0.8) * np.exp(-0.1*x) + + # 选取几个点 + i = 25 + ax4.plot(x, u, 'b-', linewidth=2, label='真实解') + ax4.scatter(x[i], u[i], s=150, color='red', zorder=5, label='计算点 $u_i$') + ax4.scatter(x[i-1], u[i-1], s=100, color='green', zorder=4, label='$u_{i-1}$') + ax4.scatter(x[i+1], u[i+1], s=100, color='orange', zorder=4, label='$u_{i+1}$') + + # 绘制差分示意 + ax4.plot([x[i-1], x[i+1]], [u[i-1], u[i+1]], 'k--', alpha=0.5) + ax4.annotate('中心差分', xy=(x[i], u[i]), xytext=(x[i], u[i]+0.3), + arrowprops=dict(arrowstyle="->", color='black'), + ha='center', fontsize=10) + + ax4.annotate('迎风格式\n使用上游点', xy=(x[i], u[i]), xytext=(x[i]-2, u[i]-0.3), + arrowprops=dict(arrowstyle="->", color='red'), + ha='center', fontsize=9) + + ax4.set_xlabel('x') + ax4.set_ylabel('u') + ax4.legend(loc='upper right') + ax4.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig('cfd_grid_illustration.png', dpi=300, bbox_inches='tight') + plt.show() + +# 运行绘图 +plot_cfd_grid_storage() \ No newline at end of file diff --git a/example/figure/1d/mesh/02/testprj.py b/example/figure/1d/mesh/02/testprj.py new file mode 100644 index 00000000..49c24860 --- /dev/null +++ b/example/figure/1d/mesh/02/testprj.py @@ -0,0 +1,21 @@ +import matplotlib.pyplot as plt +import numpy as np + +# Example 4: How to truly center a line +plt.figure(figsize=(6, 6)) + +# Method 1: Manually set symmetric coordinate ranges +plt.plot([-1, 1], [0, 0], 'r-', linewidth=3, label='Horizontal line y=0') +plt.plot([0, 0], [-1, 1], 'b-', linewidth=3, label='Vertical line x=0') + +# Key: Set symmetric axis limits +plt.xlim(-2, 2) +plt.ylim(-2, 2) + +plt.axhline(y=0, color='gray', linestyle='--', alpha=0.5) # x-axis +plt.axvline(x=0, color='gray', linestyle='--', alpha=0.5) # y-axis +plt.grid(True) +plt.title('Centered by setting symmetric ranges') +plt.legend() +plt.gca().set_aspect('equal') # Equal aspect ratio +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/02a/testprj.py b/example/figure/1d/mesh/02a/testprj.py new file mode 100644 index 00000000..004b4b8f --- /dev/null +++ b/example/figure/1d/mesh/02a/testprj.py @@ -0,0 +1,32 @@ +import matplotlib.pyplot as plt +import numpy as np + +# Example 5: Canvas coordinates vs data coordinates +fig = plt.figure(figsize=(8, 6)) + +# Center in canvas coordinates (not data coordinates!) +ax = fig.add_axes([0.1, 0.1, 0.8, 0.8]) # Graphics area centered in canvas + +# Now draw lines in the graphics area +x_center = 5 +y_center = 5 +length = 2 + +# Horizontal line +plt.plot([x_center - length/2, x_center + length/2], + [y_center, y_center], 'r-', linewidth=3, label='Horizontal line') + +# Vertical line +plt.plot([x_center, x_center], + [y_center - length/2, y_center + length/2], 'b-', linewidth=3, label='Vertical line') + +# Set symmetric ranges to center line in graphics area +plt.xlim(x_center - length, x_center + length) +plt.ylim(y_center - length, y_center + length) + +plt.grid(True) +plt.axhline(y=y_center, color='gray', linestyle='--', alpha=0.5) +plt.axvline(x=x_center, color='gray', linestyle='--', alpha=0.5) +plt.title('Line centered in graphics area') +plt.legend() +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/03/testprj.py b/example/figure/1d/mesh/03/testprj.py new file mode 100644 index 00000000..2bfeb53e --- /dev/null +++ b/example/figure/1d/mesh/03/testprj.py @@ -0,0 +1,20 @@ +import matplotlib.pyplot as plt +import numpy as np + +plt.figure(figsize=(6, 6)) + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) + +# Method 1: Manually set symmetric coordinate ranges +plt.plot([-1, 1], [0, 0], 'r-', linewidth=3, label='Horizontal line y=0') +plt.plot([0, 0], [-1, 1], 'b-', linewidth=3, label='Vertical line x=0') + +# Key: Set symmetric axis limits +plt.xlim(-6, 6) +plt.ylim(-2, 2) + +plt.title('Centered by setting symmetric ranges') +plt.legend() +plt.axis('equal') +#plt.axis('off') +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/03a/testprj.py b/example/figure/1d/mesh/03a/testprj.py new file mode 100644 index 00000000..836afa48 --- /dev/null +++ b/example/figure/1d/mesh/03a/testprj.py @@ -0,0 +1,27 @@ +import matplotlib.pyplot as plt +import numpy as np + +plt.figure(figsize=(12, 4)) # 关键:将画布设为长方形 (宽12, 高4) + +# 创建数据点 +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) + +# 画十字线 +plt.plot([-6, 6], [0, 0], 'r-', linewidth=2, label='Horizontal line y=0') # 水平线 +plt.plot([0, 0], [-2, 2], 'b-', linewidth=2, label='Vertical line x=0') # 垂直线 + +# 设置坐标范围 +plt.xlim(-6, 6) +plt.ylim(-2, 2) + +plt.title('Long strip grid: x-range [-6,6], y-range [-2,2]') +plt.legend() + +# 添加网格线 +plt.grid(True, linestyle='--', alpha=0.7) + +# 显示坐标轴刻度 +plt.xticks(np.arange(-6, 7, 1)) +plt.yticks(np.arange(-2, 3, 0.5)) + +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/03b/testprj.py b/example/figure/1d/mesh/03b/testprj.py new file mode 100644 index 00000000..9628e970 --- /dev/null +++ b/example/figure/1d/mesh/03b/testprj.py @@ -0,0 +1,20 @@ +import matplotlib.pyplot as plt +import numpy as np + +plt.figure(figsize=(8, 4)) # 长方形画布 + +# 画十字线 +plt.plot([-6, 6], [0, 0], 'r-', linewidth=2) # 水平线 +plt.plot([0, 0], [-2, 2], 'b-', linewidth=2) # 垂直线 + +# 设置坐标范围 +plt.xlim(-6, 6) +plt.ylim(-2, 2) + +# 关键:使用默认的宽高比,不强制等比例 +ax = plt.gca() +ax.set_aspect('auto') # 这是默认设置,可以省略 + +plt.title('Auto aspect ratio (default)') +plt.grid(True) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/03c/testprj.py b/example/figure/1d/mesh/03c/testprj.py new file mode 100644 index 00000000..8a39969c --- /dev/null +++ b/example/figure/1d/mesh/03c/testprj.py @@ -0,0 +1,34 @@ +import matplotlib.pyplot as plt +import numpy as np + +plt.figure(figsize=(10, 4)) + +# 画十字线 +plt.plot([-6, 6], [0, 0], 'r-', linewidth=2) +plt.plot([0, 0], [-2, 2], 'b-', linewidth=2) + +plt.xlim(-6, 6) +plt.ylim(-2, 2) + +# 计算并设置精确的宽高比 +ax = plt.gca() + +# 数据范围比例 +data_ratio = (6 - (-6)) / (2 - (-2)) # x_range / y_range = 12/4 = 3 + +# 获取图形区域的宽高(归一化坐标) +pos = ax.get_position() +fig_width, fig_height = pos.width, pos.height + +# 计算显示比例 +display_ratio = fig_width / fig_height +print(f"display_ratio={display_ratio}") +print(f"data_ratio={data_ratio}") + +# 设置宽高比:data_ratio / (display_ratio) +#ax.set_aspect(data_ratio / display_ratio) +ax.set_aspect(display_ratio / data_ratio) + +plt.title(f'Manual aspect ratio: {display_ratio/data_ratio:.2f}') +plt.grid(True) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/03d/testprj.py b/example/figure/1d/mesh/03d/testprj.py new file mode 100644 index 00000000..b75328f0 --- /dev/null +++ b/example/figure/1d/mesh/03d/testprj.py @@ -0,0 +1,44 @@ +import matplotlib.pyplot as plt +import numpy as np + +fig, axes = plt.subplots(1, 2, figsize=(14, 5)) + +# 左图:长条形网格 +ax1 = axes[0] +# 画主要网格线 +for x in np.arange(-6, 7, 1): + ax1.axvline(x=x, color='gray', linestyle='-', alpha=0.3, linewidth=0.5) +for y in np.arange(-2, 3, 0.5): + ax1.axhline(y=y, color='gray', linestyle='-', alpha=0.3, linewidth=0.5) + +# 画粗的坐标轴 +ax1.axhline(y=0, color='black', linewidth=2) +ax1.axvline(x=0, color='black', linewidth=2) + +ax1.set_xlim(-6, 6) +ax1.set_ylim(-2, 2) +ax1.set_title('1D-like grid (long strip)') +ax1.set_xlabel('x-axis') +ax1.set_ylabel('y-axis') +ax1.set_aspect('auto') # 不强制等比例 + +# 右图:比较等比例的情况 +ax2 = axes[1] +# 同样画网格线 +for x in np.arange(-6, 7, 1): + ax2.axvline(x=x, color='gray', linestyle='-', alpha=0.3, linewidth=0.5) +for y in np.arange(-2, 3, 0.5): + ax2.axhline(y=y, color='gray', linestyle='-', alpha=0.3, linewidth=0.5) + +ax2.axhline(y=0, color='black', linewidth=2) +ax2.axvline(x=0, color='black', linewidth=2) + +ax2.set_xlim(-6, 6) +ax2.set_ylim(-2, 2) +ax2.set_title('With axis("equal") - becomes square') +ax2.set_xlabel('x-axis') +ax2.set_ylabel('y-axis') +ax2.set_aspect('equal') # 强制等比例 + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/03e/testprj.py b/example/figure/1d/mesh/03e/testprj.py new file mode 100644 index 00000000..c3682f64 --- /dev/null +++ b/example/figure/1d/mesh/03e/testprj.py @@ -0,0 +1,30 @@ +import matplotlib.pyplot as plt +import numpy as np + +plt.figure(figsize=(9, 3)) # 宽:高 = 3:1,对应数据范围比例12:4=3:1 + +# 画你的数据点(如果需要) +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6]) +y_points = np.zeros_like(x_points) # 假设y坐标都是0 + +plt.scatter(x_points, y_points, color='red', s=50, zorder=5, label='Data points') + +# 画坐标轴 +plt.axhline(y=0, color='blue', linewidth=2, label='x-axis') +plt.axvline(x=0, color='green', linewidth=2, label='y-axis') + +# 设置范围 +plt.xlim(-6.5, 6.5) # 稍微扩大一点 +plt.ylim(-2, 2) + +plt.title('1D Grid Visualization') +plt.xlabel('X coordinate (wide range)') +plt.ylabel('Y coordinate (narrow range)') +plt.legend() +plt.grid(True, alpha=0.3) + +# 关键:不要设置'equal',保持默认的'auto' +# plt.axis('equal') # ← 注释掉或删除这一行! + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04/testprj.py b/example/figure/1d/mesh/04/testprj.py new file mode 100644 index 00000000..30da0a46 --- /dev/null +++ b/example/figure/1d/mesh/04/testprj.py @@ -0,0 +1,36 @@ +import matplotlib.pyplot as plt +import numpy as np + +# 创建一个简单的边界面 +fig, ax = plt.subplots(figsize=(8, 6)) + +# 主竖线 +x_main = 0 +ax.axvline(x=x_main, color='black', linewidth=3, label='Main vertical line') + +# 在主竖线旁边添加斜线 +x_offset = 0.2 # 斜线离主线的距离 +num_slashes = 20 # 斜线数量 +y_positions = np.linspace(-4, 4, num_slashes) + +for i, y in enumerate(y_positions): + # 短斜线的起点和终点 + length = 0.5 + angle = 45 # 斜线角度 + + # 计算斜线端点 + x_start = x_main + y_start = y + x_end = x_start + length * np.cos(np.radians(angle)) + y_end = y_start + length * np.sin(np.radians(angle)) + + ax.plot([x_start, x_end], [y_start, y_end], + color='red', linewidth=1.5, alpha=0.7) + +ax.set_xlim(-2, 2) +ax.set_ylim(-5, 5) +ax.set_aspect('equal') +ax.set_title('Basic vertical line with diagonal slashes') +ax.legend() +ax.grid(True, alpha=0.3) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04a/testprj.py b/example/figure/1d/mesh/04a/testprj.py new file mode 100644 index 00000000..6ef8e781 --- /dev/null +++ b/example/figure/1d/mesh/04a/testprj.py @@ -0,0 +1,45 @@ +import matplotlib.pyplot as plt +import numpy as np + +fig, ax = plt.subplots(figsize=(10, 6)) + +# 主边界竖线 +main_line_x = -3 +ax.axvline(x=main_line_x, color='darkblue', linewidth=4, alpha=0.8) + +# 创建装饰性斜线模式 +num_patterns = 15 +y_range = np.linspace(-4, 4, num_patterns) + +# 不同样式的斜线 +patterns = [ + {'color': 'red', 'length': 0.8, 'angle': 30, 'style': '-'}, + {'color': 'green', 'length': 0.6, 'angle': 45, 'style': '--'}, + {'color': 'orange', 'length': 0.4, 'angle': 60, 'style': '-.'}, +] + +for i, y in enumerate(y_range): + pattern = patterns[i % len(patterns)] # 循环使用不同样式 + + x_start = main_line_x + y_start = y + angle_rad = np.radians(pattern['angle']) + + x_end = x_start + pattern['length'] * np.cos(angle_rad) + y_end = y_start + pattern['length'] * np.sin(angle_rad) + + ax.plot([x_start, x_end], [y_start, y_end], + color=pattern['color'], + linewidth=2, + linestyle=pattern['style'], + alpha=0.7) + +# 添加一些点装饰 +for y in np.linspace(-3.5, 3.5, 8): + ax.scatter(main_line_x + 0.1, y, color='purple', s=50, alpha=0.6) + +ax.set_xlim(-4, 2) +ax.set_ylim(-5, 5) +ax.set_title('Decorative boundary pattern') +ax.grid(True, alpha=0.2) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04b/testprj.py b/example/figure/1d/mesh/04b/testprj.py new file mode 100644 index 00000000..0ad2840a --- /dev/null +++ b/example/figure/1d/mesh/04b/testprj.py @@ -0,0 +1,48 @@ +import matplotlib.pyplot as plt +import numpy as np + +fig, ax = plt.subplots(figsize=(10, 8)) + +# 两条主竖线 +left_line = -2 +right_line = 2 +ax.axvline(x=left_line, color='black', linewidth=3, label='Left boundary') +ax.axvline(x=right_line, color='black', linewidth=3, label='Right boundary') + +# 在两条竖线之间填充内容 +x_fill = np.linspace(left_line, right_line, 100) +y_fill = np.sin(x_fill * 2) * 2 +ax.fill_between(x_fill, -2, y_fill, alpha=0.2, color='skyblue', label='Area') + +# 在边界上添加对称的斜线 +num_slashes = 25 +y_positions = np.linspace(-3, 3, num_slashes) + +for y in y_positions: + # 左边界斜线(向左) + length = 0.6 + angle = 135 # 指向左下方 + + # 左边界 + x_start = left_line + y_start = y + x_end = x_start + length * np.cos(np.radians(angle)) + y_end = y_start + length * np.sin(np.radians(angle)) + ax.plot([x_start, x_end], [y_start, y_end], 'r-', linewidth=1.5, alpha=0.6) + + # 右边界斜线(向右) + angle = 45 # 指向右下方 + + x_start = right_line + y_start = y + x_end = x_start + length * np.cos(np.radians(angle)) + y_end = y_start + length * np.sin(np.radians(angle)) + ax.plot([x_start, x_end], [y_start, y_end], 'r-', linewidth=1.5, alpha=0.6) + +ax.set_xlim(-3, 3) +ax.set_ylim(-4, 4) +ax.set_aspect('equal') +ax.set_title('Boundary lines with symmetrical slashes') +ax.legend() +ax.grid(True, alpha=0.3) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04c/testprj.py b/example/figure/1d/mesh/04c/testprj.py new file mode 100644 index 00000000..77353fe9 --- /dev/null +++ b/example/figure/1d/mesh/04c/testprj.py @@ -0,0 +1,67 @@ +import matplotlib.pyplot as plt +import numpy as np + +def draw_boundary_pattern(ax, x_position, y_range=(-5, 5), + num_slashes=30, slash_length=0.5, + slash_angle=45, color='blue', + line_width=2, main_line_width=3): + """ + 绘制一个边界面图案 + + 参数: + ax: matplotlib坐标轴对象 + x_position: 主竖线的x坐标 + y_range: 斜线的y坐标范围 (min, max) + num_slashes: 斜线数量 + slash_length: 斜线长度 + slash_angle: 斜线角度(度) + color: 颜色 + """ + + # 画主竖线 + ax.axvline(x=x_position, color=color, linewidth=main_line_width, alpha=0.8) + + # 生成斜线 + y_positions = np.linspace(y_range[0], y_range[1], num_slashes) + angle_rad = np.radians(slash_angle) + + for i, y in enumerate(y_positions): + # 交替改变斜线方向 + direction = 1 if i % 2 == 0 else -1 + current_angle = slash_angle * direction + + # 计算斜线端点 + x_start = x_position + y_start = y + x_end = x_start + slash_length * np.cos(np.radians(current_angle)) + y_end = y_start + slash_length * np.sin(np.radians(current_angle)) + + # 绘制斜线 + ax.plot([x_start, x_end], [y_start, y_end], + color=color, linewidth=line_width, alpha=0.6) + + # 在斜线末端添加小圆点 + ax.scatter(x_end, y_end, color=color, s=20, alpha=0.8, zorder=5) + +# 使用自定义函数 +fig, axes = plt.subplots(2, 2, figsize=(12, 10)) +axes = axes.flatten() + +# 不同的边界样式 +configs = [ + {'x_position': -2, 'color': 'red', 'slash_angle': 30, 'num_slashes': 15}, + {'x_position': 0, 'color': 'green', 'slash_angle': 60, 'num_slashes': 20}, + {'x_position': 2, 'color': 'blue', 'slash_angle': 45, 'num_slashes': 25}, + {'x_position': -1, 'color': 'purple', 'slash_angle': 135, 'num_slashes': 18}, +] + +for ax, config in zip(axes, configs): + draw_boundary_pattern(ax, **config, y_range=(-4, 4)) + ax.set_xlim(-3, 3) + ax.set_ylim(-5, 5) + ax.set_aspect('equal') + ax.set_title(f"Boundary at x={config['x_position']}") + ax.grid(True, alpha=0.2) + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04d/testprj.py b/example/figure/1d/mesh/04d/testprj.py new file mode 100644 index 00000000..27ded332 --- /dev/null +++ b/example/figure/1d/mesh/04d/testprj.py @@ -0,0 +1,68 @@ +import matplotlib.pyplot as plt +import numpy as np + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) + +# 左图:简单边界 +# 主竖线 +boundary_x = -1 +ax1.axvline(x=boundary_x, color='black', linewidth=4) + +# 添加斜线装饰 +num_slashes = 12 +for y in np.linspace(-2, 2, num_slashes): + # 创建短斜线簇(每组3条) + for offset in [-0.1, 0, 0.1]: + ax1.plot([boundary_x, boundary_x + 0.5], + [y + offset, y + offset + 0.3], + color='darkred', linewidth=1.5, alpha=0.7) + +ax1.set_xlim(-2, 1) +ax1.set_ylim(-3, 3) +ax1.set_aspect('equal') +ax1.set_title('Simple boundary with slash clusters') +ax1.grid(True, alpha=0.3) + +# 右图:复杂边界模式 +# 多条竖线形成边界带 +for x in np.arange(-1, 1.1, 0.2): + ax2.axvline(x=x, color='gray', linewidth=1, alpha=0.3) + +# 主边界线 +main_boundary = 0 +ax2.axvline(x=main_boundary, color='darkblue', linewidth=3) + +# 在边界上添加有规律的斜线 +y_positions = np.arange(-2.5, 3, 0.5) +for i, y in enumerate(y_positions): + # 交替的斜线方向 + direction = -1 if i % 2 == 0 else 1 + angle = 45 * direction + + # 画斜线 + length = 0.4 + x_end = main_boundary + length * np.cos(np.radians(angle)) + y_end = y + length * np.sin(np.radians(angle)) + + ax2.plot([main_boundary, x_end], [y, y_end], + color='orange', linewidth=2, alpha=0.8) + + # 在斜线末端添加箭头 + ax2.scatter(x_end, y_end, color='red', s=30, alpha=0.7) + +# 添加文字说明 +ax2.text(main_boundary + 0.5, 2.5, 'Field/Area', + fontsize=12, ha='center', va='center', + bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.5)) +ax2.text(main_boundary - 0.5, 2.5, 'Boundary', + fontsize=12, ha='center', va='center', + bbox=dict(boxstyle='round', facecolor='lightcoral', alpha=0.5)) + +ax2.set_xlim(-1.5, 1.5) +ax2.set_ylim(-3, 3) +ax2.set_aspect('equal') +ax2.set_title('Complex boundary pattern with directional indicators') +ax2.grid(True, alpha=0.3) + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04e/testprj.py b/example/figure/1d/mesh/04e/testprj.py new file mode 100644 index 00000000..c6619668 --- /dev/null +++ b/example/figure/1d/mesh/04e/testprj.py @@ -0,0 +1,27 @@ +import matplotlib.pyplot as plt +import numpy as np + +# 最简单的边界面实现 +plt.figure(figsize=(8, 6)) + +# 1. 画主竖线 +plt.axvline(x=0, color='black', linewidth=4) + +# 2. 在旁边添加斜线 +y_positions = np.arange(-4, 4.5, 0.5) # 从-4到4,步长0.5 + +for y in y_positions: + # 每个位置画一条斜线 + plt.plot([0, 0.5], # x坐标:从0到0.5 + [y, y + 0.3], # y坐标:向上倾斜 + color='red', linewidth=2) + +# 3. 设置显示范围 +plt.xlim(-2, 2) +plt.ylim(-5, 5) + +# 4. 添加标题和网格 +plt.title('Simple boundary: vertical line with diagonal slashes') +plt.grid(True, alpha=0.3) + +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04f/testprj.py b/example/figure/1d/mesh/04f/testprj.py new file mode 100644 index 00000000..9ff31193 --- /dev/null +++ b/example/figure/1d/mesh/04f/testprj.py @@ -0,0 +1,104 @@ +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.collections import LineCollection # 用于批量绘制(优化性能) + +def draw_border_with_slants( + border_x: float = 5, # 主竖线的x坐标 + y_start: float = 1, # 斜线起始y坐标 + y_end: float = 7, # 斜线结束y坐标 + num_lines: int = 20, # 斜线数量 + line_length: float = 0.8, # 斜线长度 + angle: float = 30, # 斜线与水平方向夹角(度,负数反向) + border_color: str = 'black', # 主竖线颜色 + border_width: float = 2, # 主竖线宽度 + slant_color: str = 'red', # 斜线颜色 + slant_width: float = 1, # 斜线宽度 + slant_alpha: float = 0.7, # 斜线透明度 + x_lim: tuple = (0, 10), # x轴范围 + y_lim: tuple = (0, 8), # y轴范围 + title: str = '竖线+短斜线边界', # 图表标题 + save_path: str = None # 保存路径(None则不保存) +) -> None: + """ + 绘制带短斜线的竖线边界图(Matplotlib版) + + 参数说明: + - border_x: 主竖线的x坐标 + - y_start/y_end: 斜线在y轴的分布范围 + - num_lines: 斜线数量(越多越密集) + - line_length: 斜线的长度 + - angle: 斜线角度(正数向右倾,负数向左倾) + - border_color/border_width: 主竖线样式 + - slant_color/slant_width/slant_alpha: 斜线样式 + - x_lim/y_lim: 坐标轴范围 + - title: 图表标题 + - save_path: 保存路径(如'border.png'),None则仅显示 + """ + # 1. 初始化画布 + fig, ax = plt.subplots(figsize=(8, 6)) + ax.set_xlim(x_lim) + ax.set_ylim(y_lim) + ax.set_xlabel('X轴') + ax.set_ylabel('Y轴') + ax.set_title(title) + ax.grid(alpha=0.3) # 网格线(可选) + + # 2. 绘制主竖线 + ax.axvline(x=border_x, color=border_color, linewidth=border_width, label='边界线') + + # 3. 计算斜线坐标(批量生成,优化性能) + y_positions = np.linspace(y_start, y_end, num_lines) + angle_rad = np.deg2rad(angle) + dx = line_length * np.cos(angle_rad) + dy = line_length * np.sin(angle_rad) + + # 生成所有斜线的坐标对(格式:[[(x1,y1), (x2,y2)], ...]) + lines = [] + for y in y_positions: + x1, y1 = border_x, y + x2, y2 = x1 + dx, y1 + dy + lines.append([(x1, y1), (x2, y2)]) + + # 4. 批量绘制斜线(比循环plot更高效,尤其斜线数量多时) + lc = LineCollection( + lines, + colors=slant_color, + linewidths=slant_width, + alpha=slant_alpha + ) + ax.add_collection(lc) + + # 5. 等比例显示(保证角度准确)+ 图例 + plt.axis('equal') + ax.legend() + + # 6. 保存/显示 + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') # 高分辨率保存 + plt.show() + +# ------------------------------ +# 调用示例(直接运行即可) +# ------------------------------ +if __name__ == '__main__': + # 示例1:默认参数(向右倾斜的红色斜线) + draw_border_with_slants() + + # 示例2:自定义参数(向左倾斜的蓝色斜线,更密集) + draw_border_with_slants( + border_x=4, + num_lines=30, # 更多斜线 + angle=-45, # 向左倾斜 + slant_color='blue', # 蓝色斜线 + title='向左倾斜的密集斜线边界', + save_path='custom_border.png' # 保存到本地 + ) + + # 示例3:更粗的斜线+黑色主竖线 + draw_border_with_slants( + slant_width=2, + border_width=3, + line_length=1.2, + angle=60, + slant_color='green' + ) \ No newline at end of file diff --git a/example/figure/1d/mesh/05/testprj.py b/example/figure/1d/mesh/05/testprj.py new file mode 100644 index 00000000..0eba75f4 --- /dev/null +++ b/example/figure/1d/mesh/05/testprj.py @@ -0,0 +1,171 @@ +import matplotlib.pyplot as plt +import numpy as np + +class Ghost: + def __init__(self, xstart, dx, ncells, lr): + self.lr = lr + self.xstart = xstart + self.dx = dx + self.ncells = ncells + self.nnodes = self.ncells + 1 + def generate_mesh(self): + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + for i in range(0, self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = 0 + + for i in range(0, self.ncells): + self.xcc[i] = 0.5*(self.x[i]+self.x[i+1]) + self.ycc[i] = 0 + + def printinfo(self): + print(f"Ghost ncells={self.ncells}") + print(f"Ghost nnodes={self.nnodes}") + print(f"Ghost xstart={self.xstart}") + print(f"Ghost lr={self.lr}") + print(f"Ghost dx={self.dx}") + print(f"Ghost x={self.x}") + print(f"Ghost xcc={self.xcc}") + + + def plot(self): + y0 = 0 + ytext = y0 - 0.1 + for i in range(0, self.ncells): + if self.lr == "L": + mystr = f"${0-i}$" + else: + mystr = f"$N+{i+1}$" + plt.text(self.xcc[i], ytext, mystr, fontsize=12, ha='center') + + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + +class Mesh: + def __init__(self): + self.ncells = 9 + self.nnodes = self.ncells + 1 + self.xmin = 0.0 + self.xmax = 1.0 + + def generate_mesh(self): + self.dx = (self.xmax-self.xmin) / self.ncells + print(f"self.dx={self.dx}") + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + for i in range(0, self.nnodes): + self.x[i] = self.xmin + self.dx*(i) + self.y[i] = 0 + + for i in range(0, self.ncells): + self.xcc[i] = 0.5*(self.x[i]+self.x[i+1]) + self.ycc[i] = 0 + + #print(f"self.x={self.x}") + self.nghosts = 2 + + self.ghost_mesh_left = Ghost(self.x[0],-self.dx,self.nghosts,"L") + self.ghost_mesh_right = Ghost(self.x[-1],self.dx,self.nghosts,"R") + + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + print(f"ncells={self.ncells}") + print(f"nnodes={self.nnodes}") + print(f"xmin,xmax={self.xmin,self.xmax}") + self.ghost_mesh_left.printinfo() + self.ghost_mesh_right.printinfo() + + def plot_vertical_interface_lines(self): + dy = 0.1 * self.dx + for i in range(0, self.nnodes): + xm = self.x[i] + ym = self.y[i] + coef = 1 + if i == 0 or i== self.nnodes -1: + coef = 2 + plt.plot([xm, xm], [ym-coef*dy, ym+coef*dy], 'k-') # 绘制垂直线 + + def plot(self): + plt.scatter(self.xcc, self.ycc, s=50, facecolor='black', edgecolor='black', linewidth=1) + plt.plot(self.x, self.y, 'k-', linewidth=1) + dy = 0.1 * self.dx + self.plot_vertical_interface_lines() + """ + for i in range(0, self.nnodes): + xm = self.x[i] + ym = self.y[i] + coef = 1 + if i == 0 or i== self.nnodes -1: + coef = 2 + plt.plot([xm, xm], [ym-coef*dy, ym+coef*dy], 'k-') # 绘制垂直线 + """ + plt.text(self.x[0], self.y[0]+3*dy, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+3*dy, r'$x=b$', fontsize=12, ha='center') + + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + +def plot_cfd_line( x_points, y0 ): + # 绘制除中间 5 个点和特定边缘点外的其他点 (内部红色,边缘黑色) + edge_points_red = np.concatenate([x_points[:2], x_points[:-2]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, facecolor='red', edgecolor='black', linewidth=1) + + # 绘制左侧第三点 (i=-4) 和右侧第三点 (i=4) 为纯黑色点 + special_black_points = np.array([-4, 4]) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, facecolor='black', edgecolor='black', linewidth=1) + + # 绘制中间 6 个点 (i=-2, -1, 0, 1, 2, 3) + #middle_points = x_points[3:8] + middle_points = x_points[3:9] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, facecolor='black', edgecolor='black', linewidth=1) + + # 绘制中间 6 个点的黑实线连接 + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + # 添加左起第三点和第四点之间的分段连线(-4到-2) + plot_mixed_line(-4,-2) + + # 添加右起第三点和第四点之间的分段连线(2到4) + plot_mixed_line(2,4) + +def plot_cfd_figure(): + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + plt.figure(figsize=(12, 4)) + + #x_points = np.array([-20,-10, -7, -6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) + inner_points = 7 + bc_points_left = 2 + bc_points_right = 2 + points_max = inner_points + bc_points_right + x_points = np.arange(-bc_points_left, points_max+1, 1, dtype=np.float64) # 终止值(开区间),步长1 + print(x_points) # 输出:[-2. -1. 0. 1. 2.] + mesh = Mesh() + mesh.generate_mesh() + mesh.printinfo() + mesh.plot() + + y0 = 0 + + + # Key: Set symmetric axis limits + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + plt.axis('equal') + plt.axis('off') + + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05a/testprj.py b/example/figure/1d/mesh/05a/testprj.py new file mode 100644 index 00000000..afeabd94 --- /dev/null +++ b/example/figure/1d/mesh/05a/testprj.py @@ -0,0 +1,186 @@ +import matplotlib.pyplot as plt +import numpy as np + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = 0.0 + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.0 + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, lr=lr) + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + mystr = f"${- (i+1)}$" # Label format for left ghost cells + else: + mystr = f"$N+{i+1}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, mystr, fontsize=12, ha='center') + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + def plot_ghost_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + for i in range(1, self.nnodes-1): + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self): + # Define main mesh physical boundaries and grid resolution + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 9 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, lr=None) + self.nghosts = 2 # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + lr="R" + ) + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + # Plot main mesh cell-center points (black fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='black', edgecolor='black', linewidth=1) + # Plot horizontal line connecting main mesh nodes + plt.plot(self.x, self.y, 'k-', linewidth=1) + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dy = 0.1 * self.dx + plt.text(self.x[0], self.y[0]+3*dy, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+3*dy, r'$x=b$', fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 4)) + + # Initialize main mesh and generate grid coordinates + mesh = Mesh() + mesh.generate_total_mesh() + + # Print mesh information for verification + mesh.printinfo() + # Render all mesh components + mesh.plot() + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05b/testprj.py b/example/figure/1d/mesh/05b/testprj.py new file mode 100644 index 00000000..825709a9 --- /dev/null +++ b/example/figure/1d/mesh/05b/testprj.py @@ -0,0 +1,237 @@ +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x,y_start,y_end,lr): + # 3. 批量绘制短斜线 + num_lines = 10 # 斜线数量 + #y_start = 1 # 斜线起始y坐标 + #y_end = 7 # 斜线结束y坐标 + dy = y_end - y_start + y_positions = np.linspace(y_start+0.02*dy, y_end-0.02*dy, num_lines) # 均匀分布的y坐标 + line_length = 0.2*dy # 斜线长度 + if lr == "L": + angle = 180 + 30 + else: + angle = 30 # 斜线与水平方向的夹角(度),可改负数反向 + + # 计算斜线的终点坐标(三角函数转换) + angle_rad = np.deg2rad(angle) + dx = line_length * np.cos(angle_rad) + dy = line_length * np.sin(angle_rad) + + #plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # 循环绘制每条斜线 + for y in y_positions: + # 斜线起点:竖线位置 + x1 = border_x + y1 = y + # 斜线终点:基于角度计算 + x2 = x1 + dx + y2 = y1 + dy + # 绘制斜线(可自定义样式) + #plt.plot([x1, x2], [y1, y2], color='red', linewidth=1, alpha=0.7) + #plt.plot([x1, x2], [y1, y2], color='black', linewidth=1, alpha=0.7) + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = 0.0 + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.0 + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i}$" # Label format for left ghost cells + else: + cell_label = f"$N+{i+1}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self): + # Define main mesh physical boundaries and grid resolution + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 9 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, lr=None) + self.nghosts = 2 # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + lr="R" + ) + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh + nlabels = 2 + for i in range(self.ncells): + if i < nlabels: + cell_label = f"${i+1}$" + elif i > self.ncells - 1 - nlabels: + inew = i - (self.ncells - 1) + if inew == 0: + cell_label = f"$N$" + else: + cell_label = f"$N{inew}$" + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + # Plot main mesh cell-center points (black fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='black', edgecolor='black', linewidth=1) + # Plot horizontal line connecting main mesh nodes + plt.plot(self.x, self.y, 'k-', linewidth=1) + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + self.plot_cell_label() + + # Add boundary labels for main mesh + dy = 0.1 * self.dx + plt.text(self.x[0], self.y[0]+3.5*dy, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+3.5*dy, r'$x=b$', fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 4)) + + # Initialize main mesh and generate grid coordinates + mesh = Mesh() + mesh.generate_total_mesh() + + # Print mesh information for verification + mesh.printinfo() + # Render all mesh components + mesh.plot() + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05c/testprj.py b/example/figure/1d/mesh/05c/testprj.py new file mode 100644 index 00000000..b75e3e20 --- /dev/null +++ b/example/figure/1d/mesh/05c/testprj.py @@ -0,0 +1,302 @@ +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = 0.0 + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.0 + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i}$" # Label format for left ghost cells + else: + cell_label = f"$N+{i+1}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self): + # Define main mesh physical boundaries and grid resolution + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, lr=None) + self.nghosts = 2 # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + lr="R" + ) + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + if inew == 0: + cell_label = f"$N$" + else: + cell_label = f"$N{inew}$" + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-ytext_shift, f"$i$", fontsize=12, ha='center') + print(f"icenter={icenter}") + + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + #plt.scatter(self.xcc, self.ycc, s=50, facecolor='black', edgecolor='black', linewidth=1) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + + # Plot horizontal line connecting main mesh nodes + #plt.plot(self.x, self.y, 'k-', linewidth=1) + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + self.plot_cell_label() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + + # Add boundary labels for main mesh + dy = 0.1 * abs(self.dx) + plt.text(self.x[0], self.y[0]+3.5*dy, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+3.5*dy, r'$x=b$', fontsize=12, ha='center') + + dym = abs(self.dx) + + plt.text(self.x[0], self.y[0]-dym, r"$x_{\frac{1}{2}}$", fontsize=12, ha='center') + plt.text(self.x[1], self.y[1]-dym, r"$x_{\frac{3}{2}}$", fontsize=12, ha='center') + plt.text(self.x[-1], self.y[-1]-dym, r"$x_{N+\frac{1}{2}}$", fontsize=12, ha='center') + plt.text(self.x[-2], self.y[-2]-dym, r"$x_{N-\frac{1}{2}}$", fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 4)) + + # Initialize main mesh and generate grid coordinates + mesh = Mesh() + mesh.generate_total_mesh() + + # Print mesh information for verification + mesh.printinfo() + # Render all mesh components + mesh.plot() + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05d/testprj.py b/example/figure/1d/mesh/05d/testprj.py new file mode 100644 index 00000000..aeac3484 --- /dev/null +++ b/example/figure/1d/mesh/05d/testprj.py @@ -0,0 +1,363 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, ishift=0, ym=0): + # Define main mesh physical boundaries and grid resolution + print(f"Mesh ishift={ishift}") + self.ym = ym + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + self.nghosts = 2 # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1)+self.ishift + if inew == 0: + cell_label = f"$N$" + else: + cell_label = f"$N{inew}$" + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-ytext_shift, f"$i$", fontsize=12, ha='center') + print(f"icenter={icenter}") + + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + #plt.scatter(self.xcc, self.ycc, s=50, facecolor='black', edgecolor='black', linewidth=1) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + #plt.plot(self.x, self.y, 'k-', linewidth=1) + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def getstring(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringN(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + + # Add boundary labels for main mesh + dy = 0.1 * abs(self.dx) + plt.text(self.x[0], self.y[0]+3.5*dy, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+3.5*dy, r'$x=b$', fontsize=12, ha='center') + + dym = abs(self.dx) + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstring(a1) + str_a2 = self.getstring(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringN(an1) + str_na2 = self.getstringN(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + #plt.text(self.x[-1], self.y[-1]-dym, r"$x_{N+\frac{1}{2}}$", fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, r"$x_{N-\frac{1}{2}}$", fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 4)) + + # Initialize main mesh and generate grid coordinates + mesh = Mesh(0,0.2) + mesh.generate_total_mesh() + + # Print mesh information for verification + mesh.printinfo() + # Render all mesh components + mesh.plot() + + mesh1 = Mesh(-1,-0.2) + mesh1.generate_total_mesh() + + # Print mesh information for verification + mesh1.printinfo() + # Render all mesh components + mesh1.plot() + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05e/testprj.py b/example/figure/1d/mesh/05e/testprj.py new file mode 100644 index 00000000..a02b7d05 --- /dev/null +++ b/example/figure/1d/mesh/05e/testprj.py @@ -0,0 +1,362 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, ishift=0, ym=0): + # Define main mesh physical boundaries and grid resolution + print(f"Mesh ishift={ishift}") + self.ym = ym + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + self.nghosts = 2 # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + absinew = abs(inew) + sign = "-" if inew < 0 else "+" + if inew == 0: + cell_label = f"$N$" + else: + cell_label = f"$N{sign}{absinew}$" + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-ytext_shift, f"$i$", fontsize=12, ha='center') + print(f"icenter={icenter}") + + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + #plt.scatter(self.xcc, self.ycc, s=50, facecolor='black', edgecolor='black', linewidth=1) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + #plt.plot(self.x, self.y, 'k-', linewidth=1) + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def getstring(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringN(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + + # Add boundary labels for main mesh + dy = 0.1 * abs(self.dx) + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + #plt.text(self.x[0], self.y[0]+3.5*dy, r'$x=a$', fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[0]+3.5*dy, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstring(a1) + str_a2 = self.getstring(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringN(an1) + str_na2 = self.getstringN(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + #plt.text(self.x[-1], self.y[-1]-dym, r"$x_{N+\frac{1}{2}}$", fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, r"$x_{N-\frac{1}{2}}$", fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + + # Initialize main mesh and generate grid coordinates + mesh1 = Mesh(0,0.6) + mesh2 = Mesh(2,0.2) + mesh3 = Mesh(-1,-0.2) + mesh3 = Mesh(1,-0.6) + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05f/testprj.py b/example/figure/1d/mesh/05f/testprj.py new file mode 100644 index 00000000..7b6da6e3 --- /dev/null +++ b/example/figure/1d/mesh/05f/testprj.py @@ -0,0 +1,375 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, ishift=0, ym=0): + # Define main mesh physical boundaries and grid resolution + print(f"Mesh ishift={ishift}") + self.ym = ym + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + self.nghosts = 2 # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ist={1+self.ishift},ied={cell_label},ied-ist=N$" + text = f"$ied-ist=N$" + plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + + # Initialize main mesh and generate grid coordinates + mesh1 = Mesh(0,0.6) + mesh2 = Mesh(2,0.2) + mesh3 = Mesh(-1,-0.2) + mesh3 = Mesh(1,-0.6) + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05g/testprj.py b/example/figure/1d/mesh/05g/testprj.py new file mode 100644 index 00000000..932d3d21 --- /dev/null +++ b/example/figure/1d/mesh/05g/testprj.py @@ -0,0 +1,378 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0): + # Define main mesh physical boundaries and grid resolution + print(f"Mesh ishift={ishift}") + self.ym = ym + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ist={1+self.ishift},ied={cell_label},ied-ist=N$" + text = f"$ied-ist=N$" + plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + nghost3 = 3 + mesh1 = Mesh(nghost3,0,0.6) + mesh2 = Mesh(nghost3,nghost3,0.2) + mesh3 = Mesh(nghost3,-1,-0.2) + mesh3 = Mesh(nghost3,nghost3-1,-0.6) + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/periodic/01/testprj.py b/example/figure/1d/periodic/01/testprj.py new file mode 100644 index 00000000..38786f55 --- /dev/null +++ b/example/figure/1d/periodic/01/testprj.py @@ -0,0 +1,375 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0): + # Define main mesh physical boundaries and grid resolution + print(f"Mesh ishift={ishift}") + self.ym = ym + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ist={1+self.ishift},ied={cell_label},ied-ist=N$" + text = f"$ied-ist=N$" + plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + nghost3 = 3 + mesh = Mesh(nghost2,nghost2-1) + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/periodic/01a/testprj.py b/example/figure/1d/periodic/01a/testprj.py new file mode 100644 index 00000000..bd9cc0e3 --- /dev/null +++ b/example/figure/1d/periodic/01a/testprj.py @@ -0,0 +1,396 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # 新增:periodic参数,默认False + print(f"Mesh ishift={ishift}") + self.ym = ym + self.periodic = periodic # 新增:周期标志 + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + # 新增:如果periodic=True,绘制周期连接线 + if self.periodic: + self.plot_periodic_connections() + + # 新增方法:绘制周期边界折线箭头 + def plot_periodic_connections(self): + """绘制从源细胞到鬼细胞的折线箭头,表示值赋值""" + vlen = 0.3 * abs(self.dx) # 竖线长度 + h_offset = 0.02 # 水平线微调,避免重叠 + color = 'green' # 绿色区分 + lw = 1 # 线宽 + + # 右鬼赋值:cell2 (主i=1) -> N+2 (右鬼i=0), cell3 (主i=2) -> N+3 (右鬼i=1) + for src_i, tgt_i in [(1, 0), (2, 1)]: + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[tgt_i], self.ghost_mesh_right.ycc[tgt_i] + + # 竖线向上 + plt.plot([src_x, src_x], [src_y, src_y + vlen], color=color, linewidth=lw) + # 水平线到目标上方 + plt.plot([src_x, tgt_x], [src_y + vlen, tgt_y + vlen], color=color, linewidth=lw) + # 带箭头竖线向下到目标 + plt.arrow(tgt_x, tgt_y + vlen, 0, tgt_y - (tgt_y + vlen), + head_width=0.01, head_length=0.01, fc=color, ec=color, linewidth=lw) + + # 左鬼赋值:cell N (主i=6) -> u[0] (左鬼i=1, 标签0), cell N-1 (主i=5) -> u[1] (左鬼i=0, 标签1) + # 匹配用户:u[N]->u[0] (主i=6 -> 左i=1), u[N+1]->u[1] 但N+1无,用主i=5近似u[N-1]->u[1] (左i=0) + for src_i, tgt_i in [(6, 1), (5, 0)]: + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[tgt_i], self.ghost_mesh_left.ycc[tgt_i] + + # 竖线向上 + plt.plot([src_x, src_x], [src_y, src_y + vlen], color=color, linewidth=lw) + # 水平线到目标上方 (向左) + plt.plot([src_x, tgt_x], [src_y + vlen, tgt_y + vlen], color=color, linewidth=lw) + # 带箭头竖线向下到目标 + plt.arrow(tgt_x, tgt_y + vlen, 0, tgt_y - (tgt_y + vlen), + head_width=0.01, head_length=0.01, fc=color, ec=color, linewidth=lw) + +def plot_cfd_figure(periodic=False): # 新增:periodic参数,默认False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + mesh = Mesh(nghost2,nghost2-1, periodic=periodic) # 传递periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # 示例:启用周期可视化 \ No newline at end of file diff --git a/example/figure/1d/periodic/01b/testprj.py b/example/figure/1d/periodic/01b/testprj.py new file mode 100644 index 00000000..5c398b5b --- /dev/null +++ b/example/figure/1d/periodic/01b/testprj.py @@ -0,0 +1,418 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + print(f"Mesh ishift={ishift}") + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + # Added method: Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + #vlen = 0.3 * abs(self.dx) # Vertical line length + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + #h_offset = 0.02 # Horizontal line fine-tuning to avoid overlap + h_offset = 0.0 + head_width = 0.01 # Arrow head width + head_length = 0.01 # Arrow head length + lw = 1 # Line width + + # Left ghost assignments: Draw above x-axis (y > 0) + # cell N (main i=6) -> u[0] (left ghost i=1), cell N-1 (main i=5) -> u[1] (left ghost i=0) + connections_left = [(6, 1, '-', 'blue'), (5, 0, '--', 'purple')] # Different line styles and colors + for src_i, tgt_i, ls, color in connections_left: + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[tgt_i], self.ghost_mesh_left.ycc[tgt_i] + + # Source vertical line upward with arrow (from src_y + offset_y to src_y + vlen + offset_y) + start_y_src = src_y + offset_y + end_y_src = src_y + vlen + offset_y + plt.arrow(src_x, start_y_src, 0, end_y_src - start_y_src, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + # Horizontal line to target above + plt.plot([src_x, tgt_x], [end_y_src, tgt_y + vlen + offset_y], color=color, ls=ls, linewidth=lw) + print(f"end_y_src,tgt_y + vlen + offset_y={end_y_src,end_y_src}") + # Target vertical line downward with arrow (from tgt_y + vlen + offset_y to tgt_y + offset_y) + start_y_tgt = tgt_y + vlen + offset_y + end_y_tgt = tgt_y + offset_y + plt.arrow(tgt_x, start_y_tgt, 0, end_y_tgt - start_y_tgt, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + + # Right ghost assignments: Draw below x-axis (y < 0) by mirroring the logic + # cell2 (main i=1) -> N+2 (right ghost i=0), cell3 (main i=2) -> N+3 (right ghost i=1) + connections_right = [(0, 0, '-', 'green'), (1, 1, '--', 'orange')] # Different line styles and colors + for src_i, tgt_i, ls, color in connections_right: + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[tgt_i], self.ghost_mesh_right.ycc[tgt_i] + + # Source vertical line downward with arrow (from src_y - offset_y to src_y - vlen - offset_y) + start_y_src = src_y - offset_y + end_y_src = src_y - vlen - offset_y + plt.arrow(src_x, start_y_src, 0, end_y_src - start_y_src, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + # Horizontal line to target below + plt.plot([src_x, tgt_x], [end_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + # Target vertical line upward with arrow (from tgt_y - vlen - offset_y to tgt_y - offset_y) + start_y_tgt = tgt_y - vlen - offset_y + h_offset + end_y_tgt = tgt_y - offset_y + plt.arrow(tgt_x, start_y_tgt, 0, end_y_tgt - start_y_tgt, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + mesh = Mesh(nghost2,nghost2-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/periodic/01c/testprj.py b/example/figure/1d/periodic/01c/testprj.py new file mode 100644 index 00000000..0d52fc80 --- /dev/null +++ b/example/figure/1d/periodic/01c/testprj.py @@ -0,0 +1,424 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + print(f"Mesh ishift={ishift}") + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + # Added method: Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + #vlen = 0.3 * abs(self.dx) # Vertical line length + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + #h_offset = 0.02 # Horizontal line fine-tuning to avoid overlap + h_offset = 0.0 + head_width = 0.01 # Arrow head width + head_length = 0.01 # Arrow head length + lw = 1 # Line width + + # Left ghost assignments: Draw above x-axis (y > 0) + # cell N (main i=6) -> u[0] (left ghost i=1), cell N-1 (main i=5) -> u[1] (left ghost i=0) + connections_left = [(6, 0, '-', 'blue'), (5, 1, '--', 'purple')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_left): + #print(f"连接 {idx}: 从节点{src_i}到节点{tgt_i}, 线型{ls}, 颜色{color}") + + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[tgt_i], self.ghost_mesh_left.ycc[tgt_i] + + # Source vertical line upward with arrow (from src_y + offset_y to src_y + vlen + offset_y) + dy = idx * vlen + start_y_src = src_y + offset_y + end_y_src = src_y + vlen + offset_y + dy + plt.arrow(src_x, start_y_src, 0, end_y_src - start_y_src, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + # Horizontal line to target above + #plt.plot([src_x, tgt_x], [end_y_src, tgt_y + vlen + offset_y], color=color, ls=ls, linewidth=lw) + plt.plot([src_x, tgt_x], [end_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + print(f"end_y_src,tgt_y + vlen + offset_y={end_y_src,end_y_src}") + # Target vertical line downward with arrow (from tgt_y + vlen + offset_y to tgt_y + offset_y) + start_y_tgt = tgt_y + vlen + offset_y + dy + #start_y_tgt = start_y_src + end_y_tgt = tgt_y + offset_y + #print(f"") + plt.arrow(tgt_x, start_y_tgt, 0, end_y_tgt - start_y_tgt, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + + # Right ghost assignments: Draw below x-axis (y < 0) by mirroring the logic + # cell2 (main i=1) -> N+2 (right ghost i=0), cell3 (main i=2) -> N+3 (right ghost i=1) + connections_right = [(0, 0, '-', 'green'), (1, 1, '--', 'orange')] # Different line styles and colors + for src_i, tgt_i, ls, color in connections_right: + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[tgt_i], self.ghost_mesh_right.ycc[tgt_i] + + # Source vertical line downward with arrow (from src_y - offset_y to src_y - vlen - offset_y) + start_y_src = src_y - offset_y + end_y_src = src_y - vlen - offset_y + plt.arrow(src_x, start_y_src, 0, end_y_src - start_y_src, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + # Horizontal line to target below + plt.plot([src_x, tgt_x], [end_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + # Target vertical line upward with arrow (from tgt_y - vlen - offset_y to tgt_y - offset_y) + start_y_tgt = tgt_y - vlen - offset_y + h_offset + end_y_tgt = tgt_y - offset_y + plt.arrow(tgt_x, start_y_tgt, 0, end_y_tgt - start_y_tgt, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + mesh = Mesh(nghost2,nghost2-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/periodic/01d/testprj.py b/example/figure/1d/periodic/01d/testprj.py new file mode 100644 index 00000000..9e13c521 --- /dev/null +++ b/example/figure/1d/periodic/01d/testprj.py @@ -0,0 +1,464 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + print(f"Mesh ishift={ishift}") + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + # Added method: Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + #vlen = 0.3 * abs(self.dx) # Vertical line length + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + #h_offset = 0.02 # Horizontal line fine-tuning to avoid overlap + h_offset = 0.0 + head_width = 0.01 # Arrow head width + head_length = 0.01 # Arrow head length + lw = 2 # Line width + + connections_left = [(6, 0, '-', 'blue'), (5, 1, '-', 'purple')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_left): + #print(f"连接 {idx}: 从节点{src_i}到节点{tgt_i}, 线型{ls}, 颜色{color}") + + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[tgt_i], self.ghost_mesh_left.ycc[tgt_i] + + dy = idx * vlen + start_y_src = src_y + offset_y + end_y_src = src_y + vlen + offset_y + dy + + plt.plot([src_x, src_x], [start_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, src_x, start_y_src, src_x, end_y_src, color=color) + + plt.plot([src_x, tgt_x], [end_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, src_x, end_y_src, tgt_x, end_y_src, color=color) + + start_y_tgt = tgt_y + vlen + offset_y + dy + end_y_tgt = tgt_y + offset_y + + plt.plot([tgt_x, tgt_x], [start_y_tgt, end_y_tgt], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, tgt_x, start_y_tgt, tgt_x, end_y_tgt, color=color) + + connections_right = [(0, 0, '-', 'green'), (1, 1, '-', 'orange')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_right): + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[tgt_i], self.ghost_mesh_right.ycc[tgt_i] + + dy = idx * vlen + start_y_src = src_y - offset_y + end_y_src = src_y - vlen - offset_y - dy + + plt.plot([src_x, src_x], [start_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, src_x, start_y_src, src_x, end_y_src, color=color) + + plt.plot([src_x, tgt_x], [end_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, src_x, end_y_src, tgt_x, end_y_src, color=color) + + #start_y_tgt = tgt_y + vlen + offset_y + dy + #end_y_tgt = tgt_y + offset_y + + start_y_tgt = tgt_y - vlen - offset_y - dy + end_y_tgt = tgt_y - offset_y + + plt.plot([tgt_x, tgt_x], [start_y_tgt, end_y_tgt], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, tgt_x, start_y_tgt, tgt_x, end_y_tgt, color=color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + mesh = Mesh(nghost2,nghost2-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/periodic/01e/testprj.py b/example/figure/1d/periodic/01e/testprj.py new file mode 100644 index 00000000..98e62ba5 --- /dev/null +++ b/example/figure/1d/periodic/01e/testprj.py @@ -0,0 +1,480 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + print(f"Mesh ishift={ishift}") + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + # Added method: Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + #h_offset = 0.02 # Horizontal line fine-tuning to avoid overlap + h_offset = 0.0 + head_width = 0.01 # Arrow head width + head_length = 0.01 # Arrow head length + lw = 2 # Line width + + connections_left = [(6, 0, '-', 'blue'), (5, 1, '-', 'purple')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_left): + #print(f"连接 {idx}: 从节点{src_i}到节点{tgt_i}, 线型{ls}, 颜色{color}") + + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[tgt_i], self.ghost_mesh_left.ycc[tgt_i] + + dy = idx * vlen + start_y_src = src_y + offset_y + end_y_src = src_y + vlen + offset_y + dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + connections_right = [(0, 0, '-', 'green'), (1, 1, '-', 'orange')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_right): + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[tgt_i], self.ghost_mesh_right.ycc[tgt_i] + + dy = idx * vlen + start_y_src = src_y - offset_y + end_y_src = src_y - vlen - offset_y - dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + mesh = Mesh(nghost2,nghost2-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/periodic/01f/testprj.py b/example/figure/1d/periodic/01f/testprj.py new file mode 100644 index 00000000..b4027a31 --- /dev/null +++ b/example/figure/1d/periodic/01f/testprj.py @@ -0,0 +1,489 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + print(f"Mesh ishift={ishift}") + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + self.nghosts = nghosts # Number of ghost cell layers on each side + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + # Added method: Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + lw = 2 # Line width + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + icellmax = self.ncells - 1 + cindex = 0 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + connections_left = [(6, 0, '-', 'blue'), (5, 1, '-', 'purple')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_left): + #print(f"连接 {idx}: 从节点{src_i}到节点{tgt_i}, 线型{ls}, 颜色{color}") + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[tgt_i], self.ghost_mesh_left.ycc[tgt_i] + + dy = idx * vlen + end_y_src = src_y + vlen + offset_y + dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + connections_right = [(0, 0, '-', 'green'), (1, 1, '-', 'orange')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_right): + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[tgt_i], self.ghost_mesh_right.ycc[tgt_i] + + dy = idx * vlen + end_y_src = src_y - vlen - offset_y - dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + mesh = Mesh(nghost2,nghost2-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/periodic/01g/testprj.py b/example/figure/1d/periodic/01g/testprj.py new file mode 100644 index 00000000..d3aba0f0 --- /dev/null +++ b/example/figure/1d/periodic/01g/testprj.py @@ -0,0 +1,491 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + #self.nlabels = 2 + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + #self.nlabels = 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + lw = 2 # Line width + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + icellmax = self.ncells - 1 + cindex = 0 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + for i in range(len(left_list)): + isrc, itgt, color = left_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[itgt], self.ghost_mesh_left.ycc[itgt] + + dy = i * vlen + end_y_src = src_y + vlen + offset_y + dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + for i in range(len(right_list)): + isrc, itgt, color = right_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[itgt], self.ghost_mesh_right.ycc[itgt] + + dy = i * vlen + #end_y_src = src_y + vlen + offset_y + dy + end_y_src = src_y - vlen - offset_y - dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/periodic/01g0/testprj.py b/example/figure/1d/periodic/01g0/testprj.py new file mode 100644 index 00000000..a0a5cc9b --- /dev/null +++ b/example/figure/1d/periodic/01g0/testprj.py @@ -0,0 +1,490 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + lw = 2 # Line width + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + icellmax = self.ncells - 1 + cindex = 0 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + for i in range(len(left_list)): + isrc, itgt, color = left_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[itgt], self.ghost_mesh_left.ycc[itgt] + + dy = i * vlen + end_y_src = src_y + vlen + offset_y + dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + for i in range(len(right_list)): + isrc, itgt, color = right_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[itgt], self.ghost_mesh_right.ycc[itgt] + + dy = i * vlen + #end_y_src = src_y + vlen + offset_y + dy + end_y_src = src_y - vlen - offset_y - dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/weno/LinearWeights/01/xi.py b/example/figure/1d/weno/LinearWeights/01/xi.py new file mode 100644 index 00000000..1b3fcb25 --- /dev/null +++ b/example/figure/1d/weno/LinearWeights/01/xi.py @@ -0,0 +1,542 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_weno_substencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + rows, cols = sub_stencils.shape + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +def solve_weno_linear_weights(optimal_stencil: np.ndarray, sub_stencils: np.ndarray) -> np.ndarray: + """ + Solve for linear weights d such that: + optimal_stencil ≈ sum_j d[j] * sub_stencils[j] + + Prints the linear system and solved weights. + """ + + # Build target map + target_dict = build_target_offset_map(optimal_stencil) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(sub_stencils, target_dict) + return weights + +def demo_weno_linear_weights(weno_r: int): + """ + Demonstrate linear weight computation for WENO-r scheme. + + Parameters: + weno_r (int): Number of substencils (e.g., 3 for WENO5, 2 for WENO3) + """ + x_half = Fraction(1, 2) + global_stencil_width = 2 * weno_r - 1 # e.g., 5 for WENO3 + + # Left-biased (v_{i+1/2}^-) + substencils_L = generate_weno_substencils(stencil_width=weno_r, x_point=x_half) + optimal_L = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=x_half + ) + weights_L = solve_weno_linear_weights(optimal_L, substencils_L) + + # Right-biased (v_{i-1/2}^+) + substencils_R = generate_weno_substencils(stencil_width=weno_r, x_point=-x_half) + optimal_R = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=-x_half + ) + weights_R = solve_weno_linear_weights(optimal_R, substencils_R) + + return weights_L, weights_R + +if __name__ == "__main__": + maxk = 4 + for k in range(1,maxk+1): + print(f"\n=== WENO{2*k-1} ===") + demo_weno_linear_weights(weno_r=k) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/0st/01/testprj.py b/example/figure/1d/weno/interplate/0st/01/testprj.py new file mode 100644 index 00000000..4d4ad9ae --- /dev/null +++ b/example/figure/1d/weno/interplate/0st/01/testprj.py @@ -0,0 +1,120 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # x方向宽度(英寸) +fig_height = 8.0 # y方向高度(英寸),随意改实现任意长宽比 +# 示例:20x8、18x6、24x9 等都完美自适应不交叉 + +k = 3 +# ========================================================================= + +# === 缩放因子(以 16×8 为基准)=== +base_width = 16.0 +base_height = 8.0 +scale_x = fig_width / base_width +scale_y = fig_height / base_height + +# === 水平尺寸(图形略微放大 → 边距自然极小)=== +visual_cell_width = fig_width / 6.9 # 6.9 → 边距更接近 Word “窄”边距(≈0.4~0.6cm) +center_x = 3.0 * visual_cell_width # 完美居中(左2.5 + 右3.5 的平均) + +# === 垂直尺寸 === +base_dyref = 1.85 +rv, sv = [], [] +kk = k - 1 +for m in range(0, k + 1): + s_val = m + r_val = kk - s_val + rv.append(r_val) + sv.append(s_val) +num_rows = len(rv) + +dyref = base_dyref * scale_y * (4.0 / max(num_rows, 1)) +vertical_unit = dyref * 0.54 + +def plot_cell_center_rs(yref, r, s): + ms = list(range(-r, s + 1)) + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + +def plot_mesh_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + dy = 0.25 * vertical_unit + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.8*scale_x) + + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.8*scale_x) + +def plot_label_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + y_vertex = yref + 0.25 * vertical_unit + y_cell = yref - 0.68 * vertical_unit + y_rs = yref - 1.05 * vertical_unit + + # vertex 标签 + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=14*scale_y, ha='center', va='bottom') + + # cell 标签 + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=16*scale_y, ha='center', + color='red', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=14*scale_y, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=14*scale_y, ha='center') + + # (r,s) + shift = (-r + s) / 2.0 + xc = center_x + shift * visual_cell_width + plt.text(xc, y_rs, rf'$(r={r},\;s={s})$', fontsize=15*scale_y, ha='center', + color='darkblue', weight='bold') + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(fig_width, fig_height)) + +for i in range(num_rows): + yref = -i * dyref + r = rv[i] + s = sv[i] + plot_cell_center_rs(yref, r, s) + plot_mesh_rs(yref, r, s) + plot_label_rs(yref, r, s) + +# === 极小边距(真正像 Word “窄”边距)=== +margin_x = 0.12 * visual_cell_width # 左右 ≈0.3~0.5cm +margin_y = 0.25 * vertical_unit # 上下极小(保证不裁剪最后一行的 (r,s)) + +min_x = center_x - 2.5 * visual_cell_width - margin_x +max_x = center_x + 3.5 * visual_cell_width + margin_x +min_y = -(num_rows-1)*dyref - 1.3*vertical_unit - margin_y +max_y = 0 + 0.4*vertical_unit + margin_y + +plt.xlim(min_x, max_x) +plt.ylim(min_y, max_y) +plt.axis('off') + +plt.savefig('cfd_stencil_word_narrow.png', bbox_inches='tight', pad_inches=0.02, dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/0st/01a/testprj.py b/example/figure/1d/weno/interplate/0st/01a/testprj.py new file mode 100644 index 00000000..1c23101c --- /dev/null +++ b/example/figure/1d/weno/interplate/0st/01a/testprj.py @@ -0,0 +1,72 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # 图形宽度(英寸),比如 16、20、24 都行 +fig_height = 4.0 # 图形高度(英寸),单行建议 3~5 之间好看 +# ========================================================================= + +# 以 16×8 为基准的缩放(即使改成其他比例也完全自适应) +scale_x = fig_width / 16.0 +scale_y = fig_height / 8.0 + +# 水平布局:5 个点,视觉上等距,左右边距接近 Word “窄” +visual_cell_width = fig_width / 7.2 # 7.2 这个数让左右边距≈0.4~0.6cm(Word窄) +center_x = fig_width / 2 # 完美水平居中 + +# 垂直方向只需要一行,高度微调让标签不拥挤 +vertical_unit = 1.6 * scale_y # 控制标签与点的相对距离 + +# 主绘图函数(只画一行) +def plot_single_row(): + # 5 个网格点的位置:i-2, i-1, i, i+1, i+2 + offsets = [-2, -1, 0, 1, 2] + xs = [center_x + m * visual_cell_width for m in offsets] + + # 1. 画网格线(蓝色粗横线 + 黑色细竖线) + y_line = 0.0 + # 横线(覆盖整个 5 个 cell) + plt.hlines(y_line, xs[0] - 0.5*visual_cell_width, + xs[-1] + 0.5*visual_cell_width, + color='b', linewidth=2.8*scale_x) + + # 竖线(每个点左右各一条细黑线) + for x in xs: + plt.vlines(x, y_line - 0.25*vertical_unit, + y_line + 0.25*vertical_unit, + color='k', linewidth=1.8*scale_x) + + # 2. 画中心黑点 + plt.scatter(xs, [0]*5, s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + + # 3. 标签(cell 标签:i-2, i-1, i, i+1, i+2) + for m, x in zip(offsets, xs): + if m == 0: + plt.text(x, 0.68*vertical_unit, r'$i$', fontsize=16*scale_y, + ha='center', va='bottom', color='red', weight='bold') + elif m > 0: + plt.text(x, 0.68*vertical_unit, rf'$i+{m}$', fontsize=14*scale_y, + ha='center', va='bottom') + else: + plt.text(x, 0.68*vertical_unit, rf'$i{-m}$', fontsize=14*scale_y, + ha='center', va='bottom') + +# ========================== 绘图 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +fig = plt.figure(figsize=(fig_width, fig_height)) + +plot_single_row() + +# 极窄边距(真正像 Word “窄”) +margin_x = 0.12 * visual_cell_width +margin_y = 0.4 * vertical_unit +plt.xlim(center_x - 2.5*visual_cell_width - margin_x, + center_x + 2.5*visual_cell_width + margin_x) +plt.ylim(-1.2*vertical_unit - margin_y, 1.6*vertical_unit + margin_y) + +plt.axis('off') +plt.savefig('cfd_stencil_5point_single_row.png', bbox_inches='tight', + pad_inches=0.02, dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/0st/01b/testprj.py b/example/figure/1d/weno/interplate/0st/01b/testprj.py new file mode 100644 index 00000000..a6cb85c3 --- /dev/null +++ b/example/figure/1d/weno/interplate/0st/01b/testprj.py @@ -0,0 +1,95 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # 随意改,18、20、24 都完美自适应 +fig_height = 4.0 # 单行建议 3.5~5,太高会空 +# ========================================================================= + +# 缩放(保持和原来完全一致的自适应能力) +scale_x = fig_width / 16.0 +scale_y = fig_height / 8.0 + +visual_cell_width = fig_width / 6.9 # 左右边距≈0.4~0.6cm(Word 窄) +center_x = 3.0 * visual_cell_width # 原始代码的完美居中方式(左2.5 + 右3.5 的中点) + +# 垂直方向单位(和原代码一致) +base_dyref = 1.85 +dyref = base_dyref * scale_y * (4.0 / 4) # 原来是按行数缩放,这里固定为单行 +vertical_unit = dyref * 0.54 +yref = 0.0 # 只画一行,固定在 y=0 + +# ========================== 绘图函数(严格复刻你原始逻辑) ========================== +def plot_cell_center_rs(yref, r=2, s=2): # 固定 r=2, s=2 → 对应 i-2 ~ i+2 + ms = list(range(-r, s + 1)) # [-2, -1, 0, 1, 2] + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + +def plot_mesh_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) + if not ms: + return + dy = 0.25 * vertical_unit + # 竖线:位于 face 位置(m-0.5 和 m+0.5) + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.8*scale_x) + + # 蓝色粗横线:每个 cell 内部 + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.8*scale_x) + +def plot_label_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) + y_vertex = yref + 0.25 * vertical_unit # x_{i±1/2} 标签位置 + y_cell = yref - 0.68 * vertical_unit # i, i±1 标签位置 + + # 1. face 标签:x_{i-1/2}, x_{i+1/2}, ... + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) # |v| = 0.5,1.5,2.5 → 1,3,5 → 1/2, 3/2, 5/2 + sign = '+' if v > 0 else ('-' if v < 0 else '') + if frac % 2 == 1: # 奇数 → x_{i±1/2}, x_{i±3/2}, ... + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + else: + label = rf'$x_{{i{sign}{frac//2}}}$' # 暂时不会出现整数,但保留 + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=14*scale_y, ha='center', va='bottom') + + # 2. cell 标签:i-2, i-1, i, i+1, i+2(写在 cell 中心) + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=16*scale_y, ha='center', + color='red', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=14*scale_y, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=14*scale_y, ha='center') + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(fig_width, fig_height)) + +plot_cell_center_rs(yref) +plot_mesh_rs(yref) +plot_label_rs(yref) + +# 极窄边距(和原来完全一致) +margin_x = 0.12 * visual_cell_width +margin_y = 0.25 * vertical_unit +min_x = center_x - 2.5 * visual_cell_width - margin_x +max_x = center_x + 2.5 * visual_cell_width + margin_x # 原来右边是3.5,这里对称改为2.5(5点更紧凑美观) +min_y = -1.3*vertical_unit - margin_y +max_y = 0.4*vertical_unit + margin_y + +plt.xlim(min_x, max_x) +plt.ylim(min_y, max_y) +plt.axis('off') +plt.savefig('cfd_5point_stencil_standard.png', bbox_inches='tight', pad_inches=0.02, dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/0st/01c/testprj.py b/example/figure/1d/weno/interplate/0st/01c/testprj.py new file mode 100644 index 00000000..4b8559bd --- /dev/null +++ b/example/figure/1d/weno/interplate/0st/01c/testprj.py @@ -0,0 +1,92 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # 随意改宽度 +fig_height = 4.0 # 单行建议 3.8~4.5,最好别低于 3.5 +# ========================================================================= + +# 完全复刻你原始代码的缩放与布局参数(一点都没改!) +base_width = 16.0 +base_height = 8.0 +scale_x = fig_width / base_width +scale_y = fig_height / base_height + +visual_cell_width = fig_width / 6.9 # 原始值,保证 Word 窄边距 +center_x = 3.0 * visual_cell_width # 原始完美居中(左2.5 + 右3.5 的中点) + +ffsize = 30 + +base_dyref = 1.85 +dyref = base_dyref * scale_y * (4.0 / 4) # 原始公式,这里固定 4 使单行间距与原来一行完全一致 +vertical_unit = dyref * 0.54 # 完全不变 +yref = 0.0 # 只画一行 + +# ========================== 绘图函数(100% 复刻原始逻辑) ========================== +def plot_cell_center_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) # [-2,-1,0,1,2] + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + +def plot_mesh_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) + dy = 0.1 * vertical_unit + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: # 画黑色细竖线(face位置) + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.8*scale_x) + + for m in ms: # 画蓝色粗横线(每个cell) + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.8*scale_x) + +def plot_label_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) + y_vertex = yref + 0.2 * vertical_unit # x_{i±1/2} 在竖线上方,和原来完全一样 + y_cell = yref - 0.25 * vertical_unit # i, i±1 在点下方,和原来完全一样 + + # 1. face 标签 x_{i-3/2}, x_{i-1/2}, x_{i+1/2}, x_{i+3/2}, x_{i+5/2} + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) # 1,3,5 → 1/2,3/2,5/2 + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=ffsize*scale_y, ha='center', va='bottom') + + # 2. cell 标签 i-2, i-1, i, i+1, i+2 + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=ffsize*scale_y, ha='center', + color='black', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=ffsize*scale_y, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=ffsize*scale_y, ha='center') + +# ========================== 主程序(和原来一模一样) ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(fig_width, fig_height)) + +plot_cell_center_rs(yref) +plot_mesh_rs(yref) +plot_label_rs(yref) + +# 边距也完全复刻原始代码(极窄,插入Word完美) +margin_x = 0.12 * visual_cell_width +margin_y = 0.25 * vertical_unit +min_x = center_x - 2.5 * visual_cell_width - margin_x +max_x = center_x + 3.5 * visual_cell_width + margin_x # 保持原始左右不对称(右边多留一点,和原来一致) +min_y = -1.3*vertical_unit - margin_y +max_y = 0.4*vertical_unit + margin_y + +plt.xlim(min_x, max_x) +plt.ylim(min_y, max_y) +plt.axis('off') +plt.savefig('cfd_5point_perfect_same_as_original.png', bbox_inches='tight', + pad_inches=0.02, dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/0st/01d/testprj.py b/example/figure/1d/weno/interplate/0st/01d/testprj.py new file mode 100644 index 00000000..e7cccf3f --- /dev/null +++ b/example/figure/1d/weno/interplate/0st/01d/testprj.py @@ -0,0 +1,101 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # 随意改宽度 +fig_height = 4.0 # 单行建议 3.8~4.5,最好别低于 3.5 +# ========================================================================= + +# 完全复刻你原始代码的缩放与布局参数(一点都没改!) +base_width = 16.0 +base_height = 8.0 +scale_x = fig_width / base_width +scale_y = fig_height / base_height + +visual_cell_width = fig_width / 6.9 # 原始值,保证 Word 窄边距 +center_x = 3.0 * visual_cell_width # 原始完美居中(左2.5 + 右3.5 的中点) + +ffsize = 30 + +base_dyref = 1.85 +dyref = base_dyref * scale_y * (4.0 / 4) # 原始公式,这里固定 4 使单行间距与原来一行完全一致 +vertical_unit = dyref * 0.54 # 完全不变 +yref = 0.0 # 只画一行 + +# ========================== 绘图函数(100% 复刻原始逻辑) ========================== +def plot_cell_center_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) # [-2,-1,0,1,2] + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + +def plot_mesh_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) + dy = 0.1 * vertical_unit + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: # 画黑色细竖线(face位置) + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.8*scale_x) + + for m in ms: # 画蓝色粗横线(每个cell) + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.8*scale_x) + +def plot_label_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) + y_vertex = yref + 0.2 * vertical_unit # x_{i±1/2} 在竖线上方,和原来完全一样 + y_cell = yref - 0.25 * vertical_unit # i, i±1 在点下方,和原来完全一样 + y_xi = yref - 0.3 * vertical_unit + y_xidef = yref - 0.5 * vertical_unit + + # 1. face 标签 x_{i-3/2}, x_{i-1/2}, x_{i+1/2}, x_{i+3/2}, x_{i+5/2} + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) # 1,3,5 → 1/2,3/2,5/2 + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=ffsize*scale_y, ha='center', va='bottom') + + label_xi = rf'$\xi={sign}\frac{{{frac}}}{{2}}$' + plt.text(xv, y_xi, label_xi, fontsize=ffsize*scale_y, ha='center', va='bottom') + + # 2. cell 标签 i-2, i-1, i, i+1, i+2 + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=ffsize*scale_y, ha='center', + color='black', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=ffsize*scale_y, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=ffsize*scale_y, ha='center') + + plt.text(center_x, y_xidef, r'$\xi=\frac{x-x_{i}}{\Delta x}$', + fontsize=ffsize*scale_y, ha='center',color='black', weight='bold') + + +# ========================== 主程序(和原来一模一样) ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(fig_width, fig_height)) + +plot_cell_center_rs(yref) +plot_mesh_rs(yref) +plot_label_rs(yref) + +# 边距也完全复刻原始代码(极窄,插入Word完美) +margin_x = 0.12 * visual_cell_width +margin_y = 0.25 * vertical_unit +min_x = center_x - 2.5 * visual_cell_width - margin_x +max_x = center_x + 3.5 * visual_cell_width + margin_x # 保持原始左右不对称(右边多留一点,和原来一致) +min_y = -1.3*vertical_unit - margin_y +max_y = 0.4*vertical_unit + margin_y + +plt.xlim(min_x, max_x) +plt.ylim(min_y, max_y) +plt.axis('off') +plt.savefig('cfd_5point_perfect_same_as_original.png', bbox_inches='tight', + pad_inches=0.02, dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/01/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/01/compute_integral.py new file mode 100644 index 00000000..3a85b1ca --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/01/compute_integral.py @@ -0,0 +1,271 @@ +import numpy as np +from typing import Tuple, Optional +from functools import lru_cache + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """ + 计算∫_{α}^{β} ξ^power dξ的精确值 + + 数学公式: + power = 0: ∫ 1 dξ = β - α + power ≥ 1: ∫ ξ^power dξ = (β^(power+1) - α^(power+1)) / (power+1) + + 参数: + alpha: 积分下限 + beta: 积分上限 + power: 非负整数幂次 + + 返回: + 积分值(浮点数) + + 示例: + >>> compute_integral(-0.5, 0.5, 0) + 1.0 + >>> compute_integral(-0.5, 0.5, 1) + 0.0 # 奇函数对称区间 + >>> compute_integral(-0.5, 0.5, 2) + 0.08333333333333333 + """ + if power < 0: + raise ValueError(f"幂次必须为非负整数,但得到{power}") + + # 特殊情况:power=0时,积分值为β-α + if power == 0: + return beta - alpha + + # 一般情况:使用解析解 + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + + +def compute_alpha_beta(i: int, r: int) -> Tuple[float, float]: + """ + 计算第i行的积分区间[α_i, β_i] + + 公式: + α_i = -r + i - 1/2 + β_i = -r + i + 1/2 + + 参数: + i: 行索引 (0 ≤ i < k) + r: 给定的参数值 + + 返回: + (α_i, β_i) 元组 + """ + offset = -r + i + alpha = offset - 0.5 + beta = offset + 0.5 + return alpha, beta + + +def compute_matrix_M(k: int, r: int, vectorized: bool = True) -> np.ndarray: + """ + 计算k×k的矩阵M + + 矩阵M的定义: + M[i][j] = ∫_{α_i}^{β_i} ξ^j dξ, 其中 i,j = 0,...,k-1 + + 参数说明: + k: 矩阵维度(正整数) + r: 参数值(0 ≤ r < k) + vectorized: 是否使用向量化计算(默认True,更快) + + 返回: + k×k的NumPy数组 + + 示例: + >>> compute_matrix_M(3, 1) + array([[1. , 0. , 0.08333333], + [1. , 1. , 0.75 ], + [1. , 2. , 2.08333333]]) + """ + # 参数验证 + if not isinstance(k, int) or k <= 0: + raise ValueError(f"k必须是正整数,但得到{k}") + if not (0 <= r < k): + raise ValueError(f"r必须在[0, {k-1}]范围内,但得到{r}") + + # 使用向量化计算(推荐,速度快) + if vectorized: + return _compute_matrix_M_vectorized(k, r) + + # 或使用循环计算(更直观,但较慢) + return _compute_matrix_M_loop(k, r) + + +def _compute_matrix_M_loop(k: int, r: int) -> np.ndarray: + """循环版本(易于理解)""" + M = np.zeros((k, k)) + + # 逐行计算 + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + + # 逐列计算 + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + + return M + + +@lru_cache(maxsize=32) +def _compute_matrix_M_vectorized(k: int, r: int) -> np.ndarray: + """ + 向量化版本(性能最优) + + 利用NumPy广播机制,一次性计算所有元素 + 缓存结果:相同(k,r)重复调用时直接返回缓存 + """ + # 创建行索引数组 [0, 1, ..., k-1] + i = np.arange(k) + + # 计算所有α_i和β_i + # α_i = -r + i - 0.5 + # β_i = -r + i + 0.5 + alpha = -r + i - 0.5 # shape: (k,) + beta = -r + i + 0.5 # shape: (k,) + + # 创建列索引数组(幂次+1) [1, 2, ..., k] + # 因为积分公式需要 j+1 + j = np.arange(1, k + 1) # shape: (k,) + + # 计算α_i^{j+1}和β_i^{j+1} + # alpha[:, None] shape: (k, 1) + # j shape: (k,) + # 广播后结果shape: (k, k) + alpha_pow = alpha[:, None] ** j # α_i^{j+1} + beta_pow = beta[:, None] ** j # β_i^{j+1} + + # 计算积分值矩阵 + # M[i,j] = (β_i^{j+1} - α_i^{j+1}) / (j+1) + M = (beta_pow - alpha_pow) / j + + return M + + +def format_matrix(M: np.ndarray, precision: int = 6) -> str: + """ + 美化打印矩阵 + + 参数: + M: 矩阵 + precision: 小数点后保留位数 + + 返回: + 格式化字符串 + """ + return np.array2string( + M, + precision=precision, + suppress_small=True, + max_line_width=100 + ) + + +# ============= 测试和演示 ============= + +def demonstrate_matrix_properties(k: int = 4, r: int = 1): + """ + 演示矩阵M的性质 + + 性质1: 当r固定时,M[i+1][j] = M[i][j] + 偏移量 + 性质2: 每行第一个元素 M[i][0] = β_i - α_i = 1(恒定) + """ + print("="*60) + print(f"矩阵M性质演示 (k={k}, r={r})") + print("="*60) + + M = compute_matrix_M(k, r) + print(f"矩阵M:\n{format_matrix(M)}\n") + + # 验证性质2 + print("验证性质:每行第一个元素 = β_i - α_i = 1") + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + print(f" 第{i}行: M[{i}][0] = {M[i,0]:.6f}, β_i-α_i = {beta-alpha:.6f}") + + # 验证性质1(展示偏移模式) + print(f"\n验证性质:相邻行之间的差异模式") + for j in range(min(3, k)): # 只看前3列 + print(f" 第{j}列差分: {M[1:, j] - M[:-1, j]}") + + # 显示区间信息 + print(f"\n积分区间详情:") + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + print(f" 第{i}行: [α_{i}, β_{i}] = [{alpha:6.2f}, {beta:6.2f}]") + + +def compare_performance(k_values: List[int] = [10, 50, 100]): + """ + 性能对比:循环版 vs 向量化版 + + 参数: + k_values: 要测试的k值列表 + """ + print("\n" + "="*60) + print("性能对比测试") + print("="*60) + + import time + + for k in k_values: + print(f"\nk = {k}:") + + # 测试循环版本 + start = time.time() + M_loop = compute_matrix_M(k, r=0, vectorized=False) + time_loop = time.time() - start + + # 测试向量化版本 + start = time.time() + M_vec = compute_matrix_M(k, r=0, vectorized=True) + time_vec = time.time() - start + + print(f" 循环版本: {time_loop:.6f} 秒") + print(f" 向量化版: {time_vec:.6f} 秒") + print(f" 速度提升: {time_loop / time_vec:.2f} 倍") + + # 验证结果是否一致 + if np.allclose(M_loop, M_vec): + print(f" 结果一致性: ✓ 通过") + else: + print(f" 结果一致性: ✗ 失败!") + + +def test_special_cases(): + """ + 测试特殊k和r值 + """ + print("\n" + "="*60) + print("特殊值测试") + print("="*60) + + # 测试k=1(最小维度) + print("\n测试k=1, r=0:") + M = compute_matrix_M(1, 0) + print(f"矩阵M: {M}") + + # 测试r=0(区间左端点从-0.5开始) + print("\n测试k=3, r=0:") + M = compute_matrix_M(3, 0) + print(f"矩阵M:\n{format_matrix(M)}") + print("区间: [-0.5, 0.5], [0.5, 1.5], [1.5, 2.5]") + + +if __name__ == "__main__": + # 基础演示 + demonstrate_matrix_properties(k=4, r=1) + + # 性能测试 + compare_performance([10, 50, 100, 200]) + + # 特殊值测试 + test_special_cases() + + # 生成特定矩阵 + print("\n" + "="*60) + print("生成k=5, r=2的矩阵:") + print("="*60) + M = compute_matrix_M(5, 2) + print(format_matrix(M)) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/01a/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/01a/compute_integral.py new file mode 100644 index 00000000..3e0c42f5 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/01a/compute_integral.py @@ -0,0 +1,115 @@ +import numpy as np +from typing import Tuple + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """ + 计算∫_{α}^{β} ξ^power dξ的精确值 + """ + if power < 0: + raise ValueError(f"幂次必须为非负整数,但得到 {power}") + + if power == 0: + return beta - alpha + + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """ + 计算第row_index行的积分区间[α, β] + """ + middle = -r + row_index + alpha = middle - 0.5 + beta = middle + 0.5 + + return alpha, beta + + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """ + 计算k×k的矩阵M + + 矩阵定义: M[i][j] = ∫_{α_i}^{β_i} ξ^j dξ + """ + print(f"\n开始计算矩阵M (k={k}, r={r})...") + print("=" * 50) + + # 创建全零矩阵 + M = np.zeros((k, k), dtype=float) + + # 逐行计算 + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + + print(f"\n第 {i} 行: 区间 [{alpha:.2f}, {beta:.2f}]") + print("-" * 40) + + # 逐列计算 + for j in range(k): + value = compute_integral(alpha, beta, j) + M[i, j] = value + + print(f" M[{i}][{j}] = ∫_{alpha:.2f}^{beta:.2f} ξ^{j} dξ = {value:.6f}") + + print(f" 第{i}行完成: {M[i, :]}") + + print("\n" + "=" * 50) + print("矩阵计算完成!") + + return M + + +def print_matrix_nicely(M: np.ndarray, r: int, precision: int = 4): + """ + 美化打印矩阵 + """ + k = M.shape[0] + + print(f"\n{'='*60}") + print(f"最终矩阵M (k={k}, r={r})") + print(f"{'='*60}\n") + + # ✅ 修复:suppress_small 改为 suppress + np.set_printoptions(precision=precision, suppress=True, linewidth=120) + + print("矩阵数值:") + print(M) + print() + + # 打印区间信息 + print("积分区间详情:") + print(f"{'行号 i':<8} {'α_i':<10} {'β_i':<10} {'宽度 β_i-α_i':<15}") + print("-" * 45) + + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + width = beta - alpha + print(f"{i:<8} {alpha:<10.2f} {beta:<10.2f} {width:<15.2f}") + + # 验证第一列 + print(f"\n验证:每行第一列M[i][0] = β_i - α_i = 1.0") + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + expected = beta - alpha + actual = M[i, 0] + status = "✓ 正确" if abs(actual - expected) < 1e-6 else "✗ 错误" + print(f" 第{i}行: M[{i}][0] = {actual:.4f}, 期望值 = {expected:.4f} {status}") + + +def demonstrate_3x3_example(): + """ + 演示3×3矩阵的例子,r=1 + """ + print("="*70) + print("演示:计算3×3矩阵M (r=1)") + print("="*70) + + # 计算3×3矩阵 + M = compute_matrix_M(k=3, r=1) + + # 美化打印 + print_matrix_nicely(M, r=1, precision=4) + + +if __name__ == "__main__": + demonstrate_3x3_example() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/01b/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/01b/compute_integral.py new file mode 100644 index 00000000..23aad7d9 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/01b/compute_integral.py @@ -0,0 +1,121 @@ +import numpy as np +from fractions import Fraction +from typing import Tuple + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M""" + M = np.zeros((k, k), dtype=float) + + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + + return M + +def matrix_to_fractions(M: np.ndarray) -> list: + """ + 将矩阵转换为Fraction格式,便于美观显示 + + 处理技巧: + - 分母不超过1000 + - 避免过大分数 + """ + fraction_matrix = [] + for row in M: + fraction_row = [] + for val in row: + # 将小数转换为分数,限制分母大小 + frac = Fraction(val).limit_denominator(1000) + fraction_row.append(frac) + fraction_matrix.append(fraction_row) + + return fraction_matrix + +def format_fraction_matrix(frac_matrix: list) -> str: + """格式化Fraction矩阵为字符串""" + lines = [] + for row in frac_matrix: + # 将每个Fraction转换为字符串,并统一宽度 + row_str = " ".join([f"{str(frac):>10}" for frac in row]) + lines.append(f"[ {row_str} ]") + + return "\n".join(lines) + +def format_float_matrix(M: np.ndarray, precision: int = 4) -> str: + """格式化浮点矩阵为字符串""" + # 设置numpy打印选项 + with np.printoptions(precision=precision, suppress=True, linewidth=100): + return str(M) + +def compute_and_display(k: int, r: int): + """ + 主函数:计算并显示矩阵M及其逆矩阵 + + 参数: + k: 矩阵维度 + r: 参数值 (0 ≤ r < k) + """ + print(f"\n{'='*70}") + print(f"k = {k}, r = {r} 的矩阵M 及其逆矩阵") + print(f"{'='*70}\n") + + # 1. 计算矩阵M + M = compute_matrix_M(k, r) + + # 2. 计算逆矩阵 + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + print("⚠️ 矩阵不可逆!") + return + + # 3. 转换为Fraction格式 + M_frac = matrix_to_fractions(M) + M_inv_frac = matrix_to_fractions(M_inv) + + # 4. 显示实数格式 + print("📊 实数格式:") + print("-" * 30) + print("矩阵 M:") + print(format_float_matrix(M, precision=6)) + print("\n逆矩阵 M⁻¹:") + print(format_float_matrix(M_inv, precision=6)) + + # 5. 显示Fraction格式(美观) + print(f"\n{'='*70}") + print("🎨 Fraction分数格式(美观):") + print("-" * 30) + print("矩阵 M:") + print(format_fraction_matrix(M_frac)) + print("\n逆矩阵 M⁻¹:") + print(format_fraction_matrix(M_inv_frac)) + + # 6. 验证 M × M⁻¹ = I + identity = M @ M_inv + print(f"\n{'='*70}") + print("✅ 验证 M × M⁻¹ = I (单位矩阵):") + print("-" * 30) + print(format_float_matrix(identity, precision=10)) + +# ==================== 运行示例 ==================== +if __name__ == "__main__": + # 示例1:3×3矩阵,r=0 + compute_and_display(k=3, r=0) + + # 示例2:3×3矩阵,r=1 + compute_and_display(k=3, r=1) + + # 示例3:4×4矩阵,r=2 + compute_and_display(k=4, r=2) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/01c/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/01c/compute_integral.py new file mode 100644 index 00000000..5f556eee --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/01c/compute_integral.py @@ -0,0 +1,173 @@ +import numpy as np +from fractions import Fraction +from typing import List, Tuple + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + + return M + +def solve_for_coefficients(k: int, r: int, i: int = 0) -> List[str]: + """ + 求解系数a的符号表达式 + + 参数: + k: 矩阵维度 + r: 参数值 (0 ≤ r < k) + i: 基础索引(用于构造v的下标) + + 返回: + a_coeffs: a0, a1, ..., a_{k-1}的符号表达式列表 + """ + # 1. 计算矩阵M + M = compute_matrix_M(k, r) + + # 2. 计算逆矩阵 + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + # 3. 构造符号向量v的下标 + # v_indices = [i-r+0, i-r+1, ..., i-r+k-1] + v_indices = [i - r + m for m in range(k)] + + # 4. 求解每个a_j + a_coeffs = [] + + for j in range(k): # j是a的下标 + # a_j = Σ(M_inv[j][m] * v_{i-r+m}) + terms = [] + + for m in range(k): # m是求和变量 + coeff = M_inv[j, m] # 数值系数 + + # 只处理非零系数 + if abs(coeff) > 1e-10: + # 格式化系数 + if abs(coeff - 1.0) < 1e-10: + term_str = f"v[{v_indices[m]}]" + elif abs(coeff + 1.0) < 1e-10: + term_str = f"-v[{v_indices[m]}]" + else: + # 尝试转换为分数显示 + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + term_str = f"{frac.numerator}*v[{v_indices[m]}]" + else: + term_str = f"{frac}*v[{v_indices[m]}]" + + terms.append(term_str) + + # 合并项 + if not terms: + a_coeffs.append("0") + elif len(terms) == 1: + a_coeffs.append(terms[0]) + else: + a_coeffs.append(" + ".join(terms)) + + return a_coeffs + +def format_coefficients(a_coeffs: List[str], k: int, r: int, i: int = 0): + """美化打印系数表达式""" + print(f"\n{'='*70}") + print(f"k={k}, r={r}, i={i} 的求解结果") + print(f"{'='*70}\n") + + print("矩阵M:") + M = compute_matrix_M(k, r) + print(M) + + print(f"\n逆矩阵 M⁻¹:") + M_inv = np.linalg.inv(M) + print(M_inv) + + print(f"\n符号向量 v:") + print(f"v = [v[{i-r+0}], v[{i-r+1}], ..., v[{i-r+k-1}]]") + + print(f"\n求解得到的系数 a:") + print("-" * 50) + for j, expr in enumerate(a_coeffs): + print(f" a_{j} = {expr}") + + print("\n向量形式:") + print(" [a_0, a_1, ..., a_{k-1}]^T = M⁻¹ * [v[i-r+0], v[i-r+1], ..., v[i-r+k-1]]^T") + +# ============= 示例:k=3 ============= + +def example_k_3(): + """k=3的完整示例""" + k = 3 + r = 1 + i = 0 # 基础索引 + + print("="*70) + print("示例:k=3, r=1, i=0") + print("="*70) + + # 计算矩阵M + M = compute_matrix_M(k, r) + print("\n步骤1: 计算矩阵M") + print("-" * 40) + print("M[i][j] = ∫_{α_i}^{β_i} ξ^j dξ") + print("\n其中区间:") + for row in range(k): + alpha, beta = compute_alpha_beta(row, r) + print(f" 第{row}行: α_{row}={alpha:.2f}, β_{row}={beta:.2f}") + + print("\n得到的矩阵M:") + print(M) + + # 计算逆矩阵 + M_inv = np.linalg.inv(M) + print("\n步骤2: 计算逆矩阵 M⁻¹") + print("-" * 40) + print(M_inv) + + # 验证 M * M⁻¹ = I + identity = M @ M_inv + print("\n验证 M × M⁻¹ = I:") + print(identity) + + # 求解系数 + print("\n步骤3: 求解 a = M⁻¹ v") + print("-" * 40) + print("符号向量 v = [v[i-r+0], v[i-r+1], v[i-r+2]]") + print(f" = [v[{0-r+0}], v[{0-r+1}], v[{0-r+2}]]") + print(f" = [v[{-r}], v[{-r+1}], v[{-r+2}]]") + + a_coeffs = solve_for_coefficients(k, r, i) + format_coefficients(a_coeffs, k, r, i) + + # 额外:展示r=0和r=2的对比 + print("\n" + "="*70) + print("对比:r=0, 1, 2 的系数表达式") + print("="*70) + + for test_r in [0, 1, 2]: + if test_r < k: # r必须小于k + coeffs = solve_for_coefficients(k, test_r, i) + print(f"\nr={test_r}:") + for j, expr in enumerate(coeffs): + print(f" a_{j} = {expr}") + +if __name__ == "__main__": + example_k_3() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/01d/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/01d/compute_integral.py new file mode 100644 index 00000000..112a790f --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/01d/compute_integral.py @@ -0,0 +1,136 @@ +import numpy as np +from fractions import Fraction +from typing import List, Tuple + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def solve_for_coefficients(k: int, r: int, i: int = 0) -> List[str]: + """ + 求解系数a的符号表达式(输出格式:v[i±offset]) + + 核心修改:将v[-1], v[0]改为v[i-1], v[i+0]的形式 + """ + # 计算矩阵M和逆矩阵 + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs = [] + + # 对每个系数a_j(j是a的下标) + for j in range(k): + terms = [] + + # 对求和项m(m是v的下标偏移) + for m in range(k): + coeff = M_inv[j, m] # 数值系数 + + if abs(coeff) > 1e-10: # 只处理非零系数 + # 计算v的下标相对于i的偏移量 + # v_index = i - r + m + # offset = v_index - i = m - r + offset = m - r # 这是核心!不再是具体的数值下标 + + # 根据偏移量格式化符号下标 + if offset == 0: + v_str = f"v[i]" # 偏移为0 + elif offset > 0: + v_str = f"v[i+{offset}]" # 正偏移 + else: # offset < 0 + v_str = f"v[i{offset}]" # 负偏移(offset自带负号) + + # 格式化系数 + if abs(coeff - 1.0) < 1e-10: + term_str = v_str # 系数为1,不显示 + elif abs(coeff + 1.0) < 1e-10: + term_str = f"-{v_str}" # 系数为-1,只显示负号 + else: + # 分数格式化 + frac = Fraction(coeff).limit_denominator(1000) + term_str = f"{frac}*{v_str}" + + terms.append(term_str) + + # 合并项 + if not terms: + a_coeffs.append("0") + else: + a_coeffs.append(" + ".join(terms)) + + return a_coeffs + +def format_coefficients(a_coeffs: List[str], k: int, r: int, i: int = 0): + """美化打印系数表达式""" + print(f"\n{'='*70}") + print(f"k={k}, r={r}, i={i} 的求解结果") + print(f"{'='*70}\n") + + print("矩阵M:") + M = compute_matrix_M(k, r) + print(M) + + print(f"\n逆矩阵 M⁻¹:") + M_inv = np.linalg.inv(M) + print(M_inv) + + print(f"\n符号向量 v:") + print(f"v = [v[i-r+0], v[i-r+1], ..., v[i-r+{k-1}]]") + if i == 0: # 简化显示 + print(f" = [v[-{r}], v[-{r}+1], ..., v[{k-1-r}]]") + + print(f"\n求解得到的系数 a:") + print("-" * 50) + for j, expr in enumerate(a_coeffs): + print(f" a_{j} = {expr}") + + print("\n向量形式:") + print(" [a_0, a_1, ..., a_{k-1}]^T = M⁻¹ * [v[i-r], v[i-r+1], ..., v[i-r+k-1]]^T") + +def example_k_3(): + """k=3的完整示例""" + k = 3 + r = 1 + i = 0 + + print("="*70) + print("示例:k=3, r=1, i=0(输出格式为v[i±offset])") + print("="*70) + + # 求解系数 + a_coeffs = solve_for_coefficients(k, r, i) + format_coefficients(a_coeffs, k, r, i) + + # 对比不同r值 + print("\n" + "="*70) + print("对比:r=0, 1, 2 的系数表达式") + print("="*70) + + for test_r in [0, 1, 2]: + if test_r < k: + coeffs = solve_for_coefficients(k, test_r, i) + print(f"\nr={test_r}:") + for j, expr in enumerate(coeffs): + print(f" a_{j} = {expr}") + +if __name__ == "__main__": + example_k_3() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/01e/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/01e/compute_integral.py new file mode 100644 index 00000000..17020f43 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/01e/compute_integral.py @@ -0,0 +1,141 @@ +import numpy as np +from fractions import Fraction +from typing import List, Tuple + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def solve_for_coefficients(k: int, r: int, i: int = 0) -> List[str]: + """ + 求解系数a的符号表达式(输出格式:v[i±offset]) + + 关键改进:负系数前直接显示减号,不显示+ - + """ + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs = [] + + # 对每个系数a_j + for j in range(k): + terms = [] + + # 对求和项m + for m in range(k): + coeff = M_inv[j, m] + + if abs(coeff) > 1e-10: # 非零系数 + # 计算相对偏移量 + offset = m - r + + # 格式化符号下标 + if offset == 0: + v_str = "v[i]" + elif offset > 0: + v_str = f"v[i+{offset}]" + else: # offset < 0 + v_str = f"v[i{offset}]" + + # 格式化系数部分 + if abs(coeff - 1.0) < 1e-10: + term_str = v_str # 系数为1 + elif abs(coeff + 1.0) < 1e-10: + term_str = f"-{v_str}" # 系数为-1 + else: + frac = Fraction(coeff).limit_denominator(1000) + term_str = f"{frac}*{v_str}" + + terms.append(term_str) + + # 关键改进:智能连接各项,避免+ -问题 + if not terms: + a_coeffs.append("0") + else: + # 第一个项直接添加(保留其原始符号) + expr = terms[0] + + # 后续项根据符号智能连接 + for term in terms[1:]: + if term.startswith('-'): + # 负号项:直接加空格和项(负号自带) + expr += f" {term}" + else: + # 正号项:加 + 和项 + expr += f" + {term}" + + a_coeffs.append(expr) + + return a_coeffs + +def format_coefficients(a_coeffs: List[str], k: int, r: int, i: int = 0): + """美化打印系数表达式""" + print(f"\n{'='*70}") + print(f"k={k}, r={r}, i={i} 的求解结果") + print(f"{'='*70}\n") + + print("矩阵M:") + M = compute_matrix_M(k, r) + print(M) + + print(f"\n逆矩阵 M⁻¹:") + M_inv = np.linalg.inv(M) + print(M_inv) + + print(f"\n符号向量 v:") + print(f"v = [v[i-r+0], v[i-r+1], ..., v[i-r+{k-1}]]") + + print(f"\n求解得到的系数 a:") + print("-" * 50) + for j, expr in enumerate(a_coeffs): + print(f" a_{j} = {expr}") + + print("\n向量形式:") + print(" a = M⁻¹ * v") + +def example_k_3(): + """k=3的完整示例""" + k = 3 + r = 1 + i = 0 + + print("="*70) + print("示例:k=3, r=1, i=0(修正符号连接)") + print("="*70) + + a_coeffs = solve_for_coefficients(k, r, i) + format_coefficients(a_coeffs, k, r, i) + + # 对比不同r值 + print("\n" + "="*70) + print("对比:r=0, 1, 2 的系数表达式(已修正)") + print("="*70) + + for test_r in [0, 1, 2]: + if test_r < k: + coeffs = solve_for_coefficients(k, test_r, i) + print(f"\nr={test_r}:") + for j, expr in enumerate(coeffs): + print(f" a_{j} = {expr}") + +if __name__ == "__main__": + example_k_3() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02/compute_integral.py new file mode 100644 index 00000000..2a577c09 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02/compute_integral.py @@ -0,0 +1,299 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict + + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def solve_for_coefficients(k: int, r: int, i: int = 0) -> List[str]: + """ + 求解系数a的符号表达式(输出格式:v[i±offset]) + + 关键改进:负系数前直接显示减号,不显示+ - + """ + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs = [] + + # 对每个系数a_j + for j in range(k): + terms = [] + + # 对求和项m + for m in range(k): + coeff = M_inv[j, m] + + if abs(coeff) > 1e-10: # 非零系数 + # 计算相对偏移量 + offset = m - r + + # 格式化符号下标 + if offset == 0: + v_str = "v[i]" + elif offset > 0: + v_str = f"v[i+{offset}]" + else: # offset < 0 + v_str = f"v[i{offset}]" + + # 格式化系数部分 + if abs(coeff - 1.0) < 1e-10: + term_str = v_str # 系数为1 + elif abs(coeff + 1.0) < 1e-10: + term_str = f"-{v_str}" # 系数为-1 + else: + frac = Fraction(coeff).limit_denominator(1000) + term_str = f"{frac}*{v_str}" + + terms.append(term_str) + + # 关键改进:智能连接各项,避免+ -问题 + if not terms: + a_coeffs.append("0") + else: + # 第一个项直接添加(保留其原始符号) + expr = terms[0] + + # 后续项根据符号智能连接 + for term in terms[1:]: + if term.startswith('-'): + # 负号项:直接加空格和项(负号自带) + expr += f" {term}" + else: + # 正号项:加 + 和项 + expr += f" + {term}" + + a_coeffs.append(expr) + + return a_coeffs + +# ============ 新增:符号复合功能 ============ + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + a_coeffs: List[str]) -> str: + """ + 对多项式进行积分,并将a_j替换为v表达式 + + 参数: + polynomial: {指数: [(系数, [符号下标列表])]} + a_coeffs: a_j的v表达式列表 + + 返回: + 复合表达式字符串(如"1.0*(-1/2*v[i-1] - 1/2*v[i+1])^2") + """ + # 在[-0.5, 1/2]上积分 + a, b = -0.5, 0.5 + + # 用字典累积符号项的系数 + result_dict = defaultdict(float) + + for exp, expr_list in polynomial.items(): + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + + for coeff, symbols in expr_list: + # 生成符号键:如"a1*a1"或"a1*a2" + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" # 如"a1" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" # 如"a1^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) # 如"a1*a2" + + # 累加积分后的系数 + contribution = coeff * integral_factor + result_dict[symbol_key] += contribution + + # 构建复合表达式 + terms = [] + for symbol_key, total_coeff in result_dict.items(): + if abs(total_coeff) < 1e-10: + continue + + # 获取符号对应的a_j表达式 + # symbol_key如"a1^2" -> 需要找到a1的表达式 + base_symbol = symbol_key.split('*')[0].split('^')[0] # 提取"a1" + a_index = int(base_symbol[1:]) - 1 # a1 -> 索引0 + + if 0 <= a_index < len(a_coeffs): + a_expr = a_coeffs[a_index] + + # 构建项 + if '^2' in symbol_key: + # 平方项:用括号 + term = f"{total_coeff}*({a_expr})^2" + else: + # 一次项:直接用 + term = f"{total_coeff}*{a_expr}" + + terms.append(term) + + # 智能连接各项 + if not terms: + return "0" + + expr = terms[0] + for term in terms[1:]: + if term.startswith('-'): + expr += f" {term}" + else: + expr += f" + {term}" + + return expr + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """ + 生成所有r值的复合表达式f_r + + 参数: + k: 矩阵维度 + polynomial: 多项式(用a_j表示) + i: 基础索引 + + 返回: + f_r_dict: {r: 复合表达式字符串} + """ + f_r_dict = {} + + print(f"\n生成k={k}的复合表达式f_r") + print("="*70) + + for r in range(k): + print(f"\n--- r = {r} ---") + + # 1. 生成a系数的v表达式 + a_coeffs = solve_for_coefficients(k, r, i) + print("a系数的v表达式:") + for idx, expr in enumerate(a_coeffs): + print(f" a_{idx} = {expr}") + + # 2. 生成复合表达式f_r + f_r = evaluate_polynomial_integral_symbolic(polynomial, a_coeffs) + f_r_dict[r] = f_r + + print(f"\nf_{r} = ∫ P(x) dx (代入a系数后)") + print("-" * 50) + print(f"f_{r} = {f_r}") + + return f_r_dict + +# ============= 测试:用你的例子 ============= + +def test_your_example(): + """ + 测试你的例子:P1(x) = (1*a1^2)*x^0 + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + P2(x) = (4*a2^2)*x^0 + """ + k = 3 + + # 构建多项式(用a1, a2表示) + # 注意:a1对应索引1,a2对应索引2 + polynomial = { + 0: [(1.0, [1, 1]), # a1^2 + (4.0, [2, 2])], # a2^2(来自P2) + 1: [(4.0, [1, 2])], # a1*a2 + 2: [(4.0, [2, 2])] # a2^2(来自P1的平方项) + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式") + print("="*70) + + print("\n原始多项式(用a_j表示):") + # 简单打印多项式结构 + for exp, terms in polynomial.items(): + term_strs = [] + for coeff, symbols in terms: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + term_strs.append(f"{coeff}*a{symbols[0]}*a{symbols[1]}") + print(f" x^{exp}: {' + '.join(term_strs)}") + + # 生成复合表达式 + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + + # 总结输出 + print("\n" + "="*70) + print("总结:所有r的复合表达式f_r") + print("="*70) + for r, expr in f_r_dict.items(): + print(f"\nf_{r} = {expr}") + +def format_coefficients(a_coeffs: List[str], k: int, r: int, i: int = 0): + """美化打印系数表达式""" + print(f"\n{'='*70}") + print(f"k={k}, r={r}, i={i} 的求解结果") + print(f"{'='*70}\n") + + print("矩阵M:") + M = compute_matrix_M(k, r) + print(M) + + print(f"\n逆矩阵 M⁻¹:") + M_inv = np.linalg.inv(M) + print(M_inv) + + print(f"\n符号向量 v:") + print(f"v = [v[i-r+0], v[i-r+1], ..., v[i-r+{k-1}]]") + + print(f"\n求解得到的系数 a:") + print("-" * 50) + for j, expr in enumerate(a_coeffs): + print(f" a_{j} = {expr}") + + print("\n向量形式:") + print(" a = M⁻¹ * v") + +def example_k_3(): + """k=3的完整示例""" + k = 3 + r = 1 + i = 0 + + print("="*70) + print("示例:k=3, r=1, i=0(修正符号连接)") + print("="*70) + + a_coeffs = solve_for_coefficients(k, r, i) + format_coefficients(a_coeffs, k, r, i) + + # 对比不同r值 + print("\n" + "="*70) + print("对比:r=0, 1, 2 的系数表达式(已修正)") + print("="*70) + + for test_r in [0, 1, 2]: + if test_r < k: + coeffs = solve_for_coefficients(k, test_r, i) + print(f"\nr={test_r}:") + for j, expr in enumerate(coeffs): + print(f" a_{j} = {expr}") + +if __name__ == "__main__": + #example_k_3() + test_your_example() + \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02a/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02a/compute_integral.py new file mode 100644 index 00000000..67048cab --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02a/compute_integral.py @@ -0,0 +1,263 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def solve_for_coefficients(k: int, r: int, i: int = 0) -> List[str]: + """生成a系数的v表达式(已修正符号连接)""" + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs = [] + for j in range(k): + terms = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + + if abs(coeff - 1.0) < 1e-10: + term_str = v_str + elif abs(coeff + 1.0) < 1e-10: + term_str = f"-{v_str}" + else: + frac = Fraction(coeff).limit_denominator(1000) + term_str = f"{frac}*{v_str}" + + terms.append(term_str) + + # 智能连接 + if not terms: + a_coeffs.append("0") + else: + expr = terms[0] + for term in terms[1:]: + if term.startswith('-'): + expr += f" {term}" + else: + expr += f" + {term}" + a_coeffs.append(expr) + + return a_coeffs + +# ============ 核心改进:Fraction格式化 ============ + +def format_coefficient(frac: Fraction, force_display: bool = False) -> str: + """ + 智能格式化Fraction为字符串 + + 参数: + frac: Fraction对象 + force_display: 是否强制显示系数(即使为1) + + 返回: + 格式化后的系数字符串 + """ + # 处理零 + if frac == 0: + return "0" + + # 整数系数 + if frac.denominator == 1: + value = int(frac.numerator) + if value == 1 and not force_display: + return "" # 系数1,省略 + elif value == -1: + return "-" # 系数-1,只返回负号 + else: + return str(value) # 整数系数,如"3" + + # 分数系数 + return f"{frac.numerator}/{frac.denominator}" + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + a_coeffs: List[str]) -> str: + """ + 对多项式进行积分,并将a_j替换为v表达式 + + 关键修复:全程使用Fraction,避免浮点数 + """ + # ✅ 修复:使用Fraction表示积分限 + a = Fraction(-1, 2) # -1/2 + b = Fraction(1, 2) # 1/2 + + # 用字典累积符号项的系数(使用Fraction) + result_dict = defaultdict(lambda: Fraction(0, 1)) + + # 步骤1:积分并累加系数(使用Fraction) + for exp, expr_list in polynomial.items(): + # 计算积分因子:(b^(exp+1) - a^(exp+1))/(exp+1) + # 分子和分母都是Fraction + numerator = b**(exp + 1) - a**(exp + 1) # Fraction类型 + integral_factor = Fraction(numerator, exp + 1) # 构造函数接收Fraction和int + + for coeff, symbols in expr_list: + # 生成符号键 + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + # 累加分数系数 + # Fraction(coeff)将float转为Fraction(近似) + # 更精确的做法是:直接传入Fraction(str(coeff)) + contribution = Fraction(str(coeff)) * integral_factor + result_dict[symbol_key] += contribution + + # 步骤2:构建复合表达式 + terms = [] + for symbol_key, total_frac in result_dict.items(): + if total_frac == 0: + continue + + # 获取对应的a_j表达式 + base_symbol = symbol_key.split('*')[0].split('^')[0] + a_index = int(base_symbol[1:]) - 1 + a_expr = a_coeffs[a_index] + + # 格式化系数(接收Fraction) + coeff_str = format_coefficient(total_frac) + + # 构建项 + if '^2' in symbol_key: + # 平方项:需要括号 + if coeff_str: + term = f"{coeff_str}*({a_expr})^2" + else: + term = f"({a_expr})^2" + else: + # 一次项:直接连接 + if coeff_str: + term = f"{coeff_str}*{a_expr}" + else: + term = f"{a_expr}" + + terms.append(term) + + # 步骤3:智能连接各项 + if not terms: + return "0" + + # 找到第一个非负项 + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +# ============ 测试函数 ============ + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式f_r(Fraction优化版)""" + f_r_dict = {} + + print(f"\n生成k={k}的复合表达式f_r(Fraction优化版)") + print("="*70) + + for r in range(k): + print(f"\n--- r = {r} ---") + + # 1. 生成a系数的v表达式 + a_coeffs = solve_for_coefficients(k, r, i) + print("a系数的v表达式:") + for idx, expr in enumerate(a_coeffs): + print(f" a_{idx} = {expr}") + + # 2. 生成复合表达式f_r + f_r = evaluate_polynomial_integral_symbolic(polynomial, a_coeffs) + f_r_dict[r] = f_r + + print(f"\nf_{r}(Fraction格式):") + print("-" * 50) + # 美化输出:分行显示 + if '+' in f_r: + lines = f_r.split('+') + for line in lines: + print(f" {line.strip()}") + else: + print(f" {f_r}") + + # 总结 + print("\n" + "="*70) + print("总结:所有r的复合表达式f_r(Fraction格式)") + print("="*70) + for r, expr in f_r_dict.items(): + print(f"\nf_{r} = {expr}") + + return f_r_dict + +def test_composite(): + """测试你的例子""" + k = 3 + + # 多项式:a1^2 + 4*a1*a2*x + (4+4)*a2^2*x^2 + polynomial = { + 0: [(1.0, [1, 1]), # a1^2 + (4.0, [2, 2])], # a2^2(系数4) + 1: [(4.0, [1, 2])], # a1*a2 + 2: [(4.0, [2, 2])] # a2^2(系数4) + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(Fraction优化)") + print("="*70) + + print("\n原始多项式(用a_j表示):") + for exp, terms in polynomial.items(): + term_strs = [] + for coeff, symbols in terms: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + term_strs.append(f"{coeff}*a{symbols[0]}*a{symbols[1]}") + print(f" x^{exp}: {' + '.join(term_strs)}") + + # 生成复合表达式 + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02b/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02b/compute_integral.py new file mode 100644 index 00000000..19c9aac5 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02b/compute_integral.py @@ -0,0 +1,344 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def solve_for_coefficients(k: int, r: int, i: int = 0) -> List[str]: + """生成a系数的v表达式""" + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs = [] + for j in range(k): + terms = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + + if abs(coeff - 1.0) < 1e-10: + term_str = v_str + elif abs(coeff + 1.0) < 1e-10: + term_str = f"-{v_str}" + else: + frac = Fraction(coeff).limit_denominator(1000) + term_str = f"{frac}*{v_str}" + + terms.append(term_str) + + if not terms: + a_coeffs.append("0") + else: + expr = terms[0] + for term in terms[1:]: + if term.startswith('-'): + expr += f" {term}" + else: + expr += f" + {term}" + a_coeffs.append(expr) + + return a_coeffs + +# ============ 核心改进:分离积分与代入 ============ + +def format_coefficient(frac: Fraction, force_display: bool = False) -> str: + """智能格式化Fraction为字符串""" + if frac == 0: + return "0" + + if frac.denominator == 1: + value = int(frac.numerator) + if value == 1 and not force_display: + return "" + elif value == -1: + return "-" + else: + return str(value) + + return f"{frac.numerator}/{frac.denominator}" + +def integrate_polynomial_x(polynomial: Dict[int, List[Tuple[float, List[int]]]]) -> List[Tuple[Fraction, List[int]]]: + """ + ✅ 第一步:对x在[-1/2, 1/2]上积分,消去x变量 + + 数学原理: + - 对x^0项:∫_{-1/2}^{1/2} coeff * a_j * x^0 dx = coeff * a_j * 1 + - 对x^1项:∫_{-1/2}^{1/2} coeff * a_j * x^1 dx = coeff * a_j * 0 + - 对x^2项:∫_{-1/2}^{1/2} coeff * a_j * x^2 dx = coeff * a_j * (1/12) + + 参数: + polynomial: {幂次: [(系数, [a索引,...]), ...]} + 例如: {0: [(1.0, [1,1]), (4.0, [2,2])], 1: [(4.0, [1,2])], 2: [(4.0, [2,2])]} + + 返回: + 积分后的项列表: [(分数系数, [a索引,...]), ...] + """ + # ✅ 积分限: [-1/2, 1/2] + a = Fraction(-1, 2) + b = Fraction(1, 2) + + integrated_terms = [] + + print("\n积分过程详细计算:") + print("-" * 50) + + # 遍历多项式的每个幂次项 + for exp, expr_list in polynomial.items(): + # ✅ 计算 ∫x^exp dx 在[-1/2, 1/2]上的值 + numerator = b**(exp + 1) - a**(exp + 1) + integral_factor = Fraction(numerator, exp + 1) + + print(f" x^{exp} 积分因子: ∫ξ^{exp}dξ = {integral_factor}") + + # 对当前幂次下的每个a_j表达式项应用积分 + for coeff, symbols in expr_list: + # 将浮点系数转为精确分数 + coeff_frac = Fraction(str(coeff)) + + # 积分后的系数 = 原系数 × 积分因子 + new_coeff = coeff_frac * integral_factor + + # 生成符号表示 + if len(symbols) == 1: + symbol_str = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_str = f"a{symbols[0]}^2" + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + + print(f" {coeff_frac} * {symbol_str} × {integral_factor} = {new_coeff} * {symbol_str}") + + integrated_terms.append((new_coeff, symbols)) + + return integrated_terms + +def substitute_coefficients(integrated_terms: List[Tuple[Fraction, List[int]]], + a_coeffs: List[str]) -> str: + """ + ✅ 第二步:将积分后的a_j表达式代入v表达式 + + 参数: + integrated_terms: 积分后项列表 [(系数, [a索引,...]), ...] + a_coeffs: a系数的v表达式列表 [a0_expr, a1_expr, a2_expr, ...] + + 返回: + 最终的复合表达式字符串 + """ + # 累积同类项(如所有a1^2项合并) + result_dict = defaultdict(lambda: Fraction(0, 1)) + + print("\n合并同类项:") + print("-" * 50) + + for coeff, symbols in integrated_terms: + # 生成唯一键用于合并 + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + # 累积系数 + result_dict[symbol_key] += coeff + + # 构建最终表达式 + terms = [] + for symbol_key, total_coeff in result_dict.items(): + if total_coeff == 0: + continue + + print(f" {symbol_key}: 系数 = {total_coeff}") + + # ✅ 正确映射:多项式中的a1对应a_coeffs[1],a2对应a_coeffs[2] + base_symbol = symbol_key.split('*')[0].split('^')[0] # "a1"或"a2" + a_index = int(base_symbol[1:]) # a1→1,a2→2 + + # ✅ 安全检查 + if a_index >= len(a_coeffs): + raise ValueError(f"多项式索引{a_index}超出系数范围[0, {len(a_coeffs)-1}]") + + a_expr = a_coeffs[a_index] + coeff_str = format_coefficient(total_coeff) + + # 构建项字符串 + if '^2' in symbol_key: + # 平方项需要括号: coeff*(expr)^2 + term = f"{coeff_str}*({a_expr})^2" if coeff_str else f"({a_expr})^2" + else: + # 一次项: coeff*expr + term = f"{coeff_str}*{a_expr}" if coeff_str else f"{a_expr}" + + terms.append(term) + + return smart_join_terms(terms) + +def smart_join_terms(terms: List[str]) -> str: + """智能连接各项,处理符号""" + if not terms: + return "0" + + # 找到第一个非负项作为起点 + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + # 负号项前置 + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + # 正号项依次添加 + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + # 全为负号项 + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + a_coeffs: List[str]) -> str: + """ + ✅ 主函数:先积分,后代入 + + 步骤: + 1. 对x在[-1/2, 1/2]上积分,消去x变量 + 2. 将a_j替换为v表达式 + """ + integrated_terms = integrate_polynomial_x(polynomial) + return substitute_coefficients(integrated_terms, a_coeffs) + +# ============ 测试函数 ============ + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式f_r""" + f_r_dict = {} + + print(f"\n生成k={k}的复合表达式f_r") + print("="*70) + + for r in range(k): + print(f"\n--- r = {r} ---") + + # 1. 生成a系数的v表达式 + a_coeffs = solve_for_coefficients(k, r, i) + print("\na系数的v表达式:") + for idx, expr in enumerate(a_coeffs): + print(f" a_{idx} = {expr}") + + # 2. 生成复合表达式f_r + f_r = evaluate_polynomial_integral_symbolic(polynomial, a_coeffs) + f_r_dict[r] = f_r + + print(f"\nf_{r}(最终复合表达式):") + print("-" * 50) + print_expression_pretty(f_r, indent=" ") + + # 总结 + print("\n" + "="*70) + print("总结:所有r的复合表达式f_r") + print("="*70) + for r, expr in f_r_dict.items(): + print(f"\nf_{r} =") + print_expression_pretty(expr, indent=" ") + + return f_r_dict + +def print_expression_pretty(expr: str, indent: str = ""): + """美化打印表达式""" + if '+' not in expr: + print(f"{indent}{expr}") + return + + # 按顶层'+'分割 + lines = [] + bracket_depth = 0 + current = "" + + for char in expr: + current += char + if char == '(': + bracket_depth += 1 + elif char == ')': + bracket_depth -= 1 + elif char == '+' and bracket_depth == 0: + lines.append(current[:-1].strip()) + current = "" + + if current: + lines.append(current.strip()) + + # 打印 + for i, line in enumerate(lines): + if line.startswith('-'): + print(f"{indent}{line}") + elif i == 0: + print(f"{indent}{line}") + else: + print(f"{indent}+ {line}") + +def test_composite(): + """测试例子""" + k = 3 + + # 多项式:(a1^2 + 4*a2^2) + 4*a1*a2*x + 4*a2^2*x^2 + polynomial = { + 0: [(1.0, [1, 1]), # a1^2 + (4.0, [2, 2])], # a2^2(来自二阶导数部分) + 1: [(4.0, [1, 2])], # a1*a2 + 2: [(4.0, [2, 2])] # a2^2(来自一阶导数平方的x^2项) + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(严格先积分后代入)") + print("="*70) + + print("\n原始多项式(含x幂次):") + for exp, terms in polynomial.items(): + term_strs = [] + for coeff, symbols in terms: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + term_strs.append(f"{coeff}*a{symbols[0]}*a{symbols[1]}") + print(f" x^{exp} 项: {' + '.join(term_strs)}") + + # 生成复合表达式 + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02c/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02c/compute_integral.py new file mode 100644 index 00000000..c82338c8 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02c/compute_integral.py @@ -0,0 +1,427 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +from math import gcd +from functools import reduce +import re + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +# ============ 核心改进:优化整数化与符号处理 ============ + +def format_coefficient(frac: Fraction) -> str: + """智能格式化Fraction为字符串""" + if frac == 0: + return "0" + if frac.denominator == 1: + value = int(frac.numerator) + return str(value) + return f"{frac.numerator}/{frac.denominator}" + +def compute_lcm(numbers: List[int]) -> int: + """计算最小公倍数""" + if not numbers: + return 1 + return reduce(lambda a, b: a * b // gcd(a, b), numbers) + +def parse_and_optimize_expression(expr: str, exponent: int = 1) -> Tuple[str, Fraction]: + """ + ✅ 修复版:使用正则表达式安全解析表达式 + + 参数: + expr: 原始表达式,如 "-3/2*v[i] + 2*v[i+1] - 1/2*v[i+2]" + exponent: 指数(1为线性,2为平方) + + 返回: + (优化后表达式, 提取的因子) + """ + if expr == "0": + return "0", Fraction(1, 1) + + # 1. 如果表达式以正负号开头,添加隐含的+号 + if expr[0] not in '+-': + expr = '+' + expr + + # 2. 使用正则表达式安全解析表达式 + # 模式: ([+-]) 可选符号 + (系数) + (变量名) + # 系数支持: 整数、分数、带符号 + # 变量名: v[i] 或 v[i+1] 或 v[i-1] 等 + pattern = r'([+-])?(?:(\d+/\d+|\d+|[+-]?\d+/\d+|[+-]?\d+)\*)?(v\[i[+-]?\d*\]|v\[i\])' + + matches = re.findall(pattern, expr) + + coeff_vars = [] # List of (Fraction系数, 变量字符串) + denominators = [] # 所有分母 + + for match in matches: + sign, coeff_part, var = match + + # 处理系数 + if coeff_part: + # 清理系数字符串 + coeff_str = coeff_part.replace('+', '').replace('-', '') + if '/' in coeff_str: + num, den = map(int, coeff_str.split('/')) + coeff = Fraction(num, den) + else: + coeff = Fraction(int(coeff_str), 1) + else: + # 没有显式系数,如 "+v[i]" + coeff = Fraction(1, 1) + + # 应用符号 + if sign == '-': + coeff = -coeff + + coeff_vars.append((coeff, var)) + + # 记录分母 + if coeff.denominator != 1: + denominators.append(coeff.denominator) + + # 检查是否有任何项被解析 + if not coeff_vars: + print(f"警告:表达式 '{expr}' 未解析出任何项") + return expr, Fraction(1, 1) + + # 3. 计算最小公倍数 + lcm = compute_lcm(denominators) if denominators else 1 + + # 4. 转换为整数系数 + int_coeffs = [] + for coeff, var in coeff_vars: + int_coeff = coeff * lcm + int_coeffs.append((int(int_coeff.numerator), var)) + + # 5. 符号优化:统计正负号数量 + neg_count = sum(1 for c, _ in int_coeffs if c < 0) + pos_count = sum(1 for c, _ in int_coeffs if c > 0) + + # 如果负数多于正数,提取负号 + factor = Fraction(1, lcm) + if neg_count > pos_count: + factor = -factor + int_coeffs = [(-c, v) for c, v in int_coeffs] + + # 6. 根据指数调整因子 + factor_with_exponent = factor ** exponent + + # 7. 重建表达式字符串 + expr_parts = [] + for coeff, var in int_coeffs: + if coeff == 1: + expr_parts.append(f"+{var}") + elif coeff == -1: + expr_parts.append(f"-{var}") + elif coeff > 0: + expr_parts.append(f"+{coeff}*{var}") + else: # coeff < 0 + expr_parts.append(f"{coeff}*{var}") + + # 拼接并清理开头 + result_expr = ''.join(expr_parts) + if result_expr.startswith('+'): + result_expr = result_expr[1:] + + return result_expr, factor_with_exponent + +def solve_for_coefficients_optimized(k: int, r: int, i: int = 0) -> List[Tuple[str, Fraction]]: + """ + ✅ 生成优化的a系数表达式(整数化+符号优化) + + 返回: [(优化后表达式, 提取因子), ...] + """ + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs_optimized = [] + for j in range(k): + nonzero_items = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + nonzero_items.append((coeff, v_str)) + + if not nonzero_items: + a_coeffs_optimized.append(("0", Fraction(1, 1))) + continue + + # 构建原始表达式字符串 + expr_parts = [] + for coeff, v_str in nonzero_items: + frac = Fraction(coeff).limit_denominator(1000) + if coeff == 1.0: + expr_parts.append(f"+{v_str}") + elif coeff == -1.0: + expr_parts.append(f"-{v_str}") + else: + expr_parts.append(f"{frac}*{v_str}") + + expr = ''.join(expr_parts) + if expr.startswith('+'): + expr = expr[1:] + + # 优化表达式(exponent=1,因为是a_j本身) + optimized_expr, factor = parse_and_optimize_expression(expr, exponent=1) + + a_coeffs_optimized.append((optimized_expr, factor)) + + return a_coeffs_optimized + +# ============ 多项式积分与代入 ============ + +def integrate_polynomial_x(polynomial: Dict[int, List[Tuple[float, List[int]]]]) -> List[Tuple[Fraction, List[int]]]: + """ + ✅ 第一步:对x在[-1/2, 1/2]上积分 + """ + a = Fraction(-1, 2) + b = Fraction(1, 2) + + integrated_terms = [] + print("\n积分过程详细计算:") + print("-" * 50) + + for exp, expr_list in polynomial.items(): + numerator = b**(exp + 1) - a**(exp + 1) + integral_factor = Fraction(numerator, exp + 1) + + print(f" x^{exp} 在[-1/2,1/2]上的积分: {integral_factor}") + + for coeff, symbols in expr_list: + coeff_frac = Fraction(str(coeff)) + new_coeff = coeff_frac * integral_factor + + # 生成符号表示 + if len(symbols) == 1: + symbol_str = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_str = f"a{symbols[0]}^2" + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + + if new_coeff != 0: + print(f" {coeff_frac} * {symbol_str} × {integral_factor} = {new_coeff} * {symbol_str}") + integrated_terms.append((new_coeff, symbols)) + + return integrated_terms + +def substitute_coefficients_optimized(integrated_terms: List[Tuple[Fraction, List[int]]], + a_coeffs_opt: List[Tuple[str, Fraction]]) -> str: + """ + ✅ 第二步:将积分后的a_j表达式代入v表达式(支持优化格式) + """ + result_dict = defaultdict(lambda: Fraction(0, 1)) + + print("\n合并同类项:") + print("-" * 50) + + for coeff, symbols in integrated_terms: + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + result_dict[symbol_key] += coeff + + # 构建最终表达式 + final_terms = [] + for symbol_key, total_coeff in result_dict.items(): + if total_coeff == 0: + continue + + print(f" {symbol_key}: 总系数 = {total_coeff}") + + base_symbol = symbol_key.split('*')[0].split('^')[0] + a_index = int(base_symbol[1:]) + + if a_index >= len(a_coeffs_opt): + raise ValueError(f"多项式索引{a_index}超出范围[0, {len(a_coeffs_opt)-1}]") + + a_expr, a_factor = a_coeffs_opt[a_index] + + is_squared = '^2' in symbol_key + exponent = 2 if is_squared else 1 + + # ✅ 计算最终系数 + final_coeff = total_coeff * (a_factor ** exponent) + coeff_str = format_coefficient(final_coeff) + + # ✅ 构建最终项 + if is_squared: + if coeff_str == "1": + term = f"({a_expr})^2" + elif coeff_str == "-1": + term = f"-({a_expr})^2" + else: + term = f"{coeff_str}*({a_expr})^2" + else: + if coeff_str == "1": + term = f"{a_expr}" + elif coeff_str == "-1": + term = f"-{a_expr}" + else: + term = f"{coeff_str}*{a_expr}" + + final_terms.append(term) + + return smart_join_terms(final_terms) + +def smart_join_terms(terms: List[str]) -> str: + """智能连接各项""" + if not terms: + return "0" + + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + k: int, r: int, i: int = 0) -> str: + """ + ✅ 完整流程:积分 → 代入(支持优化) + """ + a_coeffs_opt = solve_for_coefficients_optimized(k, r, i) + + print(f"\nr = {r} 的优化a系数:") + for idx, (expr, factor) in enumerate(a_coeffs_opt): + if factor == 1: + print(f" a_{idx} = ({expr})") + else: + print(f" a_{idx} = {format_coefficient(factor)} * ({expr})") + + integrated_terms = integrate_polynomial_x(polynomial) + return substitute_coefficients_optimized(integrated_terms, a_coeffs_opt) + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式f_r(完整优化版)""" + f_r_dict = {} + + print(f"\n生成k={k}的复合表达式f_r(整数化+符号优化)") + print("="*70) + + for r in range(k): + print(f"\n--- r = {r} ---") + + f_r = evaluate_polynomial_integral_symbolic(polynomial, k, r, i) + f_r_dict[r] = f_r + + print(f"\nf_{r}(最终优化表达式):") + print("-" * 50) + print_expression_pretty(f_r, indent=" ") + + # 总结 + print("\n" + "="*70) + print("总结:所有r的优化表达式f_r") + print("="*70) + for r, expr in f_r_dict.items(): + print(f"\nf_{r} =") + print_expression_pretty(expr, indent=" ") + + return f_r_dict + +def print_expression_pretty(expr: str, indent: str = ""): + """美化打印表达式""" + if '+' not in expr: + print(f"{indent}{expr}") + return + + lines = [] + bracket_depth = 0 + current = "" + + for char in expr: + current += char + if char == '(': + bracket_depth += 1 + elif char == ')': + bracket_depth -= 1 + elif char == '+' and bracket_depth == 0: + lines.append(current[:-1].strip()) + current = "" + + if current: + lines.append(current.strip()) + + for i, line in enumerate(lines): + if line.startswith('-'): + print(f"{indent}{line}") + elif i == 0: + print(f"{indent}{line}") + else: + print(f"{indent}+ {line}") + +def test_composite(): + """测试例子""" + k = 3 + + polynomial = { + 0: [(1.0, [1, 1]), # a1^2 + (4.0, [2, 2])], # a2^2 + 1: [(4.0, [1, 2])], # a1*a2 + 2: [(4.0, [2, 2])] # a2^2 + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(完整优化版)") + print("="*70) + + print("\n原始多项式(含x幂次):") + for exp, terms in polynomial.items(): + term_strs = [] + for coeff, symbols in terms: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + term_strs.append(f"{coeff}*a{symbols[0]}*a{symbols[1]}") + print(f" x^{exp} 项: {' + '.join(term_strs)}") + + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02d/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02d/compute_integral.py new file mode 100644 index 00000000..3e280a05 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02d/compute_integral.py @@ -0,0 +1,388 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +from math import gcd +from functools import reduce +import re + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def format_coefficient(frac: Fraction) -> str: + """智能格式化Fraction为字符串""" + if frac == 0: + return "0" + if frac.denominator == 1: + return str(int(frac.numerator)) + return f"{frac.numerator}/{frac.denominator}" + +def compute_lcm(numbers: List[int]) -> int: + """计算最小公倍数""" + if not numbers: + return 1 + return reduce(lambda a, b: a * b // gcd(a, b), numbers) + +def parse_and_optimize_expression(expr: str, exponent: int = 1) -> Tuple[str, Fraction]: + """使用正则表达式安全解析表达式""" + if expr == "0": + return "0", Fraction(1, 1) + + if expr[0] not in '+-': + expr = '+' + expr + + pattern = r'([+-])?(?:(\d+/\d+|\d+|[+-]?\d+/\d+|[+-]?\d+)\*)?(v\[i[+-]?\d*\]|v\[i\])' + matches = re.findall(pattern, expr) + + coeff_vars = [] + denominators = [] + + for match in matches: + sign, coeff_part, var = match + + if coeff_part: + coeff_str = coeff_part.replace('+', '').replace('-', '') + if '/' in coeff_str: + num, den = map(int, coeff_str.split('/')) + coeff = Fraction(num, den) + else: + coeff = Fraction(int(coeff_str), 1) + else: + coeff = Fraction(1, 1) + + if sign == '-': + coeff = -coeff + + coeff_vars.append((coeff, var)) + + if coeff.denominator != 1: + denominators.append(coeff.denominator) + + if not coeff_vars: + return expr, Fraction(1, 1) + + lcm = compute_lcm(denominators) if denominators else 1 + + int_coeffs = [] + for coeff, var in coeff_vars: + int_coeff = coeff * lcm + int_coeffs.append((int(int_coeff.numerator), var)) + + neg_count = sum(1 for c, _ in int_coeffs if c < 0) + pos_count = sum(1 for c, _ in int_coeffs if c > 0) + + factor = Fraction(1, lcm) + if neg_count > pos_count: + factor = -factor + int_coeffs = [(-c, v) for c, v in int_coeffs] + + factor_with_exponent = factor ** exponent + + expr_parts = [] + for coeff, var in int_coeffs: + if coeff == 1: + expr_parts.append(f"+{var}") + elif coeff == -1: + expr_parts.append(f"-{var}") + elif coeff > 0: + expr_parts.append(f"+{coeff}*{var}") + else: + expr_parts.append(f"{coeff}*{var}") + + result_expr = ''.join(expr_parts) + if result_expr.startswith('+'): + result_expr = result_expr[1:] + + return result_expr, factor_with_exponent + +def solve_for_coefficients_optimized(k: int, r: int, i: int = 0) -> List[Tuple[str, Fraction]]: + """生成优化的a系数表达式""" + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs_optimized = [] + for j in range(k): + nonzero_items = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + nonzero_items.append((coeff, v_str)) + + if not nonzero_items: + a_coeffs_optimized.append(("0", Fraction(1, 1))) + continue + + expr_parts = [] + for coeff, v_str in nonzero_items: + frac = Fraction(coeff).limit_denominator(1000) + if coeff == 1.0: + expr_parts.append(f"+{v_str}") + elif coeff == -1.0: + expr_parts.append(f"-{v_str}") + else: + expr_parts.append(f"{frac}*{v_str}") + + expr = ''.join(expr_parts) + if expr.startswith('+'): + expr = expr[1:] + + optimized_expr, factor = parse_and_optimize_expression(expr, exponent=1) + a_coeffs_optimized.append((optimized_expr, factor)) + + return a_coeffs_optimized + +# ============ 多项式积分与代入 ============ + +def integrate_polynomial_x(polynomial: Dict[int, List[Tuple[float, List[int]]]]) -> List[Tuple[Fraction, List[int]]]: + """对x在[-1/2, 1/2]上积分""" + a = Fraction(-1, 2) + b = Fraction(1, 2) + integrated_terms = [] + + for exp, expr_list in polynomial.items(): + numerator = b**(exp + 1) - a**(exp + 1) + integral_factor = Fraction(numerator, exp + 1) + + for coeff, symbols in expr_list: + coeff_frac = Fraction(str(coeff)) + new_coeff = coeff_frac * integral_factor + + if new_coeff != 0: + integrated_terms.append((new_coeff, symbols)) + + return integrated_terms + +def substitute_coefficients_optimized(integrated_terms: List[Tuple[Fraction, List[int]]], + a_coeffs_opt: List[Tuple[str, Fraction]]) -> Tuple[str, List]: + """代入并排序""" + result_dict = defaultdict(lambda: Fraction(0, 1)) + + for coeff, symbols in integrated_terms: + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + result_dict[symbol_key] += coeff + + # 构建带系数的项列表 + terms_with_coeffs = [] + + for symbol_key, total_coeff in result_dict.items(): + if total_coeff == 0: + continue + + base_symbol = symbol_key.split('*')[0].split('^')[0] + a_index = int(base_symbol[1:]) + + if a_index >= len(a_coeffs_opt): + raise ValueError(f"多项式索引{a_index}超出范围") + + a_expr, a_factor = a_coeffs_opt[a_index] + + is_squared = '^2' in symbol_key + exponent = 2 if is_squared else 1 + + final_coeff = total_coeff * (a_factor ** exponent) + coeff_abs = abs(final_coeff) + + coeff_str = format_coefficient(final_coeff) + + # 构建最终项 + if is_squared: + term_str = f"{coeff_str}*({a_expr})^2" if coeff_str not in ["1", "-1"] else \ + (f"({a_expr})^2" if coeff_str == "1" else f"-({a_expr})^2") + else: + term_str = f"{coeff_str}*{a_expr}" if coeff_str not in ["1", "-1"] else \ + (f"{a_expr}" if coeff_str == "1" else f"-{a_expr}") + + terms_with_coeffs.append((coeff_abs, final_coeff, term_str)) + + # ✅ 按系数绝对值降序排序 + terms_with_coeffs.sort(key=lambda x: x[0], reverse=True) + + # ✅ 提取排序后的项 + final_terms = [term_str for _, _, term_str in terms_with_coeffs] + + return smart_join_terms(final_terms), terms_with_coeffs + +def smart_join_terms(terms: List[str]) -> str: + """智能连接各项""" + if not terms: + return "0" + + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + k: int, r: int, i: int = 0) -> Tuple[str, List]: + """完整流程:积分 → 代入""" + a_coeffs_opt = solve_for_coefficients_optimized(k, r, i) + integrated_terms = integrate_polynomial_x(polynomial) + return substitute_coefficients_optimized(integrated_terms, a_coeffs_opt) + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式β_r(完整优化版)""" + f_r_dict = {} + all_terms_info = {} + + print(f"\n生成k={k}的复合表达式β_r") + print("="*70) + + for r in range(k): + final_expr, terms_with_coeffs = evaluate_polynomial_integral_symbolic(polynomial, k, r, i) + f_r_dict[r] = final_expr + all_terms_info[r] = terms_with_coeffs + + print(f"\nβ_{r} = {final_expr}") + + # ============ LaTeX格式总结 ============ + print("\n" + "="*70) + print("LaTeX格式总结(按系数绝对值排序)") + print("="*70) + + latex_dict = {} + for r, terms_info in all_terms_info.items(): + latex_parts = [] + for coeff_abs, final_coeff, term_str in terms_info: + # ✅ 修复:正确分离系数和主体,避免重复 + if '*(' in term_str: + # 分离系数和主体 + coeff_part, main_part = term_str.split('*', 1) + main_part = main_part.replace('v[i', 'v_{i').replace(']', '}') + latex_term = main_part + else: + # 无显式系数 + latex_term = term_str.replace('v[i', 'v_{i').replace(']', '}') + + # 转换系数(确保只出现一次) + coeff_str = format_coefficient(final_coeff) + if coeff_str == "1": + latex_parts.append(latex_term) + elif coeff_str == "-1": + latex_parts.append(f"-{latex_term}") + else: + latex_parts.append(f"{coeff_str}{latex_term}") + + latex_expr = " + ".join(latex_parts) + latex_expr = latex_expr.replace("+ -", "- ") + latex_dict[r] = latex_expr + + print(f"\n$\\beta_{r} = {latex_expr}$") + + # ============ 最终汇总(便于复制) ============ + print("\n" + "="*70) + print("最终汇总(单行格式)") + print("="*70) + + for r in range(k): + latex_expr = latex_dict[r] + print(f"β{r} = {latex_expr}") + + # ============ LaTeX代码块(便于复制) ============ + print("\n" + "="*70) + print("LaTeX代码块") + print("="*70) + print("\n```latex") + for r in range(k): + latex_expr = latex_dict[r] + print(f"\\beta_{r} = {latex_expr}") + print("```") + + return f_r_dict + +def print_expression_pretty(expr: str, indent: str = "", single_line: bool = False): + """支持单行输出""" + if single_line: + print(f"{indent}{expr}") + return + + if '+' not in expr: + print(f"{indent}{expr}") + return + + lines = [] + bracket_depth = 0 + current = "" + + for char in expr: + current += char + if char == '(': + bracket_depth += 1 + elif char == ')': + bracket_depth -= 1 + elif char == '+' and bracket_depth == 0: + lines.append(current[:-1].strip()) + current = "" + + if current: + lines.append(current.strip()) + + for i, line in enumerate(lines): + if line.startswith('-'): + print(f"{indent}{line}") + elif i == 0: + print(f"{indent}{line}") + else: + print(f"{indent}+ {line}") + +def test_composite(): + """测试例子""" + k = 3 + + polynomial = { + 0: [(1.0, [1, 1]), (4.0, [2, 2])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(完整优化版)") + print("="*70) + + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02e/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02e/compute_integral.py new file mode 100644 index 00000000..30089036 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02e/compute_integral.py @@ -0,0 +1,482 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +from math import gcd +from functools import reduce +import re + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def format_coefficient(frac: Fraction, latex_mode: bool = False) -> str: + """ + ✅ 智能格式化Fraction为字符串 + 参数: + latex_mode: 是否使用LaTeX格式(\cfrac) + """ + if frac == 0: + return "0" + if frac.denominator == 1: + return str(int(frac.numerator)) + + # ✅ LaTeX模式:使用\cfrac + if latex_mode: + return f"\\cfrac{{{frac.numerator}}}{{{frac.denominator}}}" + + return f"{frac.numerator}/{frac.denominator}" + +def compute_lcm(numbers: List[int]) -> int: + """计算最小公倍数""" + if not numbers: + return 1 + return reduce(lambda a, b: a * b // gcd(a, b), numbers) + +def parse_and_optimize_expression(expr: str, exponent: int = 1) -> Tuple[str, Fraction]: + """使用正则表达式安全解析表达式""" + if expr == "0": + return "0", Fraction(1, 1) + + if expr[0] not in '+-': + expr = '+' + expr + + pattern = r'([+-])?(?:(\d+/\d+|\d+|[+-]?\d+/\d+|[+-]?\d+)\*)?(v\[i[+-]?\d*\]|v\[i\])' + matches = re.findall(pattern, expr) + + coeff_vars = [] + denominators = [] + + for match in matches: + sign, coeff_part, var = match + + if coeff_part: + coeff_str = coeff_part.replace('+', '').replace('-', '') + if '/' in coeff_str: + num, den = map(int, coeff_str.split('/')) + coeff = Fraction(num, den) + else: + coeff = Fraction(int(coeff_str), 1) + else: + coeff = Fraction(1, 1) + + if sign == '-': + coeff = -coeff + + coeff_vars.append((coeff, var)) + + if coeff.denominator != 1: + denominators.append(coeff.denominator) + + if not coeff_vars: + return expr, Fraction(1, 1) + + lcm = compute_lcm(denominators) if denominators else 1 + + int_coeffs = [] + for coeff, var in coeff_vars: + int_coeff = coeff * lcm + int_coeffs.append((int(int_coeff.numerator), var)) + + neg_count = sum(1 for c, _ in int_coeffs if c < 0) + pos_count = sum(1 for c, _ in int_coeffs if c > 0) + + factor = Fraction(1, lcm) + if neg_count > pos_count: + factor = -factor + int_coeffs = [(-c, v) for c, v in int_coeffs] + + factor_with_exponent = factor ** exponent + + expr_parts = [] + for coeff, var in int_coeffs: + if coeff == 1: + expr_parts.append(f"+{var}") + elif coeff == -1: + expr_parts.append(f"-{var}") + elif coeff > 0: + expr_parts.append(f"+{coeff}*{var}") + else: + expr_parts.append(f"{coeff}*{var}") + + result_expr = ''.join(expr_parts) + if result_expr.startswith('+'): + result_expr = result_expr[1:] + + return result_expr, factor_with_exponent + +def solve_for_coefficients_optimized(k: int, r: int, i: int = 0) -> List[Tuple[str, Fraction]]: + """生成优化的a系数表达式""" + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs_optimized = [] + for j in range(k): + nonzero_items = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + nonzero_items.append((coeff, v_str)) + + if not nonzero_items: + a_coeffs_optimized.append(("0", Fraction(1, 1))) + continue + + expr_parts = [] + for coeff, v_str in nonzero_items: + frac = Fraction(coeff).limit_denominator(1000) + if coeff == 1.0: + expr_parts.append(f"+{v_str}") + elif coeff == -1.0: + expr_parts.append(f"-{v_str}") + else: + expr_parts.append(f"{frac}*{v_str}") + + expr = ''.join(expr_parts) + if expr.startswith('+'): + expr = expr[1:] + + optimized_expr, factor = parse_and_optimize_expression(expr, exponent=1) + a_coeffs_optimized.append((optimized_expr, factor)) + + return a_coeffs_optimized + +def integrate_polynomial_x(polynomial: Dict[int, List[Tuple[float, List[int]]]]) -> List[Tuple[Fraction, List[int]]]: + """对x在[-1/2, 1/2]上积分""" + a = Fraction(-1, 2) + b = Fraction(1, 2) + integrated_terms = [] + + for exp, expr_list in polynomial.items(): + numerator = b**(exp + 1) - a**(exp + 1) + integral_factor = Fraction(numerator, exp + 1) + + for coeff, symbols in expr_list: + coeff_frac = Fraction(str(coeff)) + new_coeff = coeff_frac * integral_factor + + if new_coeff != 0: + integrated_terms.append((new_coeff, symbols)) + + return integrated_terms + +def substitute_coefficients_optimized(integrated_terms: List[Tuple[Fraction, List[int]]], + a_coeffs_opt: List[Tuple[str, Fraction]]) -> Tuple[str, List]: + """代入并排序""" + result_dict = defaultdict(lambda: Fraction(0, 1)) + + for coeff, symbols in integrated_terms: + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + result_dict[symbol_key] += coeff + + # 构建带系数的项列表 + terms_with_coeffs = [] + + for symbol_key, total_coeff in result_dict.items(): + if total_coeff == 0: + continue + + base_symbol = symbol_key.split('*')[0].split('^')[0] + a_index = int(base_symbol[1:]) + + if a_index >= len(a_coeffs_opt): + raise ValueError(f"多项式索引{a_index}超出范围") + + a_expr, a_factor = a_coeffs_opt[a_index] + + is_squared = '^2' in symbol_key + exponent = 2 if is_squared else 1 + + final_coeff = total_coeff * (a_factor ** exponent) + coeff_abs = abs(final_coeff) + + coeff_str = format_coefficient(final_coeff) + + # 构建最终项 + if is_squared: + term_str = f"{coeff_str}*({a_expr})^2" if coeff_str not in ["1", "-1"] else \ + (f"({a_expr})^2" if coeff_str == "1" else f"-({a_expr})^2") + else: + term_str = f"{coeff_str}*{a_expr}" if coeff_str not in ["1", "-1"] else \ + (f"{a_expr}" if coeff_str == "1" else f"-{a_expr}") + + terms_with_coeffs.append((coeff_abs, final_coeff, term_str)) + + # ✅ 按系数绝对值降序排序 + terms_with_coeffs.sort(key=lambda x: x[0], reverse=True) + + # ✅ 提取排序后的项 + final_terms = [term_str for _, _, term_str in terms_with_coeffs] + + return smart_join_terms(final_terms), terms_with_coeffs + +def smart_join_terms(terms: List[str]) -> str: + """智能连接各项""" + if not terms: + return "0" + + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +def optimize_two_term_expression(expr: str) -> str: + """ + ✅ 优化两项表达式:确保首项为正 + 例如: "-v[i-1]+v[i+1]" -> "v[i+1]-v[i-1]" + """ + if expr.count('v[') != 2: + return expr # 不是两项表达式 + + # 解析两项 + pattern = r'([+-]?)((?:\d+/)?\d+\*)?v\[i([+-]\d*)\]' + matches = re.findall(pattern, expr) + + if len(matches) != 2: + return expr + + terms = [] + for sign, coeff_part, index in matches: + if not sign: + sign = '+' + + # 系数 + if coeff_part: + coeff = coeff_part.rstrip('*') + else: + coeff = '1' + + terms.append((sign, coeff, index)) + + # 如果首项为负,交换顺序 + if terms[0][0] == '-': + # 保持数学等价:-a + b = b - a + sign1, coeff1, idx1 = terms[0] + sign2, coeff2, idx2 = terms[1] + + # 新表达式: +coeff2*v[i+idx2] - coeff1*v[i+idx1] + new_parts = [] + + if coeff2 == '1': + new_parts.append(f"v[i{idx2}]") + else: + new_parts.append(f"{coeff2}*v[i{idx2}]") + + if coeff1 == '1': + new_parts.append(f"-v[i{idx1}]") + else: + new_parts.append(f"-{coeff1}*v[i{idx1}]") + + return ''.join(new_parts) + + return expr + +def convert_to_latex(term_str: str) -> str: + """ + ✅ 将表达式转换为LaTeX格式(支持两项优化) + + 参数: + term_str: 如 "13/12*(v[i]-2*v[i+1]+v[i+2])^2" 或 "1/4*(-v[i-1]+v[i+1])^2" + + 返回: + LaTeX字符串,如 "\\cfrac{13}{12}(v_{i}-2v_{i+1}+v_{i+2})^2" + """ + # 分离系数和主体 + if '*(' in term_str: + coeff_part, main_part = term_str.split('*', 1) + main_part = main_part.strip() + else: + coeff_part = "1" + main_part = term_str + + # ✅ 提取括号内的表达式 + match = re.match(r'^\((.+)\)\^2$', main_part) + if match: + inner_expr = match.group(1) + + # ✅ 优化两项表达式(如 -v[i-1]+v[i+1]) + inner_expr = optimize_two_term_expression(inner_expr) + + # 转换v[i]为v_{i} + inner_latex = inner_expr.replace('*', '') + inner_latex = re.sub(r'v\[i([+-]?\d*)\]', r'v_{i\1}', inner_latex) + + # 构建LaTeX主体 + latex_main = f"({inner_latex})^2" + else: + # 非平方项 + latex_main = term_str.replace('*', '') + latex_main = re.sub(r'v\[i([+-]?\d*)\]', r'v_{i\1}', latex_main) + + return latex_main + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + k: int, r: int, i: int = 0) -> Tuple[str, List]: + """完整流程:积分 → 代入""" + a_coeffs_opt = solve_for_coefficients_optimized(k, r, i) + integrated_terms = integrate_polynomial_x(polynomial) + return substitute_coefficients_optimized(integrated_terms, a_coeffs_opt) + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式β_r(完整优化版)""" + f_r_dict = {} + all_terms_info = {} + + print(f"\n生成k={k}的复合表达式β_r") + print("="*70) + + for r in range(k): + final_expr, terms_with_coeffs = evaluate_polynomial_integral_symbolic(polynomial, k, r, i) + f_r_dict[r] = final_expr + all_terms_info[r] = terms_with_coeffs + + print(f"\nβ_{r} = {final_expr}") + + # ============ LaTeX格式总结 ============ + print("\n" + "="*70) + print("LaTeX格式总结(按系数绝对值排序)") + print("="*70) + + latex_dict = {} + for r, terms_info in all_terms_info.items(): + latex_parts = [] + for coeff_abs, final_coeff, term_str in terms_info: + # ✅ 转换主体表达式 + latex_term = convert_to_latex(term_str) + + # ✅ 转换系数(使用\cfrac格式) + coeff_str = format_coefficient(final_coeff, latex_mode=True) + if coeff_str == "1": + latex_parts.append(latex_term) + elif coeff_str == "-1": + latex_parts.append(f"-{latex_term}") + else: + latex_parts.append(f"{coeff_str}{latex_term}") + + latex_expr = " + ".join(latex_parts) + latex_expr = latex_expr.replace("+ -", "- ") + latex_dict[r] = latex_expr + + print(f"\n$\\beta_{r} = {latex_expr}$") + + # ============ 最终汇总(单行格式) ============ + print("\n" + "="*70) + print("最终汇总(单行格式)") + print("="*70) + + for r in range(k): + latex_expr = latex_dict[r] + print(f"β{r} = {latex_expr}") + + # ============ LaTeX代码块 ============ + print("\n" + "="*70) + print("LaTeX代码块(统一在array环境中)") + print("="*70) + print("\n```latex") + print("\\begin{array}{l}") + for r in range(k): + if r == k - 1: # 最后一行不加\\ + print(f" \\beta_{r} = {latex_dict[r]}") + else: + print(f" \\beta_{r} = {latex_dict[r]}\\\\") + print("\\end{array}") + print("```") + + return f_r_dict + +def print_expression_pretty(expr: str, indent: str = "", single_line: bool = False): + """支持单行输出""" + if single_line: + print(f"{indent}{expr}") + return + + if '+' not in expr: + print(f"{indent}{expr}") + return + + lines = [] + bracket_depth = 0 + current = "" + + for char in expr: + current += char + if char == '(': + bracket_depth += 1 + elif char == ')': + bracket_depth -= 1 + elif char == '+' and bracket_depth == 0: + lines.append(current[:-1].strip()) + current = "" + + if current: + lines.append(current.strip()) + + for i, line in enumerate(lines): + if line.startswith('-'): + print(f"{indent}{line}") + elif i == 0: + print(f"{indent}{line}") + else: + print(f"{indent}+ {line}") + +def test_composite(): + """测试例子""" + k = 3 + + polynomial = { + 0: [(1.0, [1, 1]), (4.0, [2, 2])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(完整优化版)") + print("="*70) + + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02f/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02f/compute_integral.py new file mode 100644 index 00000000..1e88084c --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02f/compute_integral.py @@ -0,0 +1,541 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +from math import gcd +from functools import reduce +import re + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def format_coefficient(frac: Fraction, latex_mode: bool = False) -> str: + """ + 智能格式化Fraction为字符串 + 参数: + latex_mode: 是否使用LaTeX格式(\\cfrac) + """ + if frac == 0: + return "0" + if frac.denominator == 1: + return str(int(frac.numerator)) + + if latex_mode: + return f"\\cfrac{{{frac.numerator}}}{{{frac.denominator}}}" + + return f"{frac.numerator}/{frac.denominator}" + +def compute_lcm(numbers: List[int]) -> int: + """计算最小公倍数""" + if not numbers: + return 1 + return reduce(lambda a, b: a * b // gcd(a, b), numbers) + +def parse_and_optimize_expression(expr: str, exponent: int = 1) -> Tuple[str, Fraction]: + """使用正则表达式安全解析表达式""" + if expr == "0": + return "0", Fraction(1, 1) + + if expr[0] not in '+-': + expr = '+' + expr + + pattern = r'([+-])?(?:(\d+/\d+|\d+|[+-]?\d+/\d+|[+-]?\d+)\*)?(v\[i[+-]?\d*\]|v\[i\])' + matches = re.findall(pattern, expr) + + coeff_vars = [] + denominators = [] + + for match in matches: + sign, coeff_part, var = match + + if coeff_part: + coeff_str = coeff_part.replace('+', '').replace('-', '') + if '/' in coeff_str: + num, den = map(int, coeff_str.split('/')) + coeff = Fraction(num, den) + else: + coeff = Fraction(int(coeff_str), 1) + else: + coeff = Fraction(1, 1) + + if sign == '-': + coeff = -coeff + + coeff_vars.append((coeff, var)) + + if coeff.denominator != 1: + denominators.append(coeff.denominator) + + if not coeff_vars: + return expr, Fraction(1, 1) + + lcm = compute_lcm(denominators) if denominators else 1 + + int_coeffs = [] + for coeff, var in coeff_vars: + int_coeff = coeff * lcm + int_coeffs.append((int(int_coeff.numerator), var)) + + neg_count = sum(1 for c, _ in int_coeffs if c < 0) + pos_count = sum(1 for c, _ in int_coeffs if c > 0) + + factor = Fraction(1, lcm) + if neg_count > pos_count: + factor = -factor + int_coeffs = [(-c, v) for c, v in int_coeffs] + + factor_with_exponent = factor ** exponent + + expr_parts = [] + for coeff, var in int_coeffs: + if coeff == 1: + expr_parts.append(f"+{var}") + elif coeff == -1: + expr_parts.append(f"-{var}") + elif coeff > 0: + expr_parts.append(f"+{coeff}*{var}") + else: + expr_parts.append(f"{coeff}*{var}") + + result_expr = ''.join(expr_parts) + if result_expr.startswith('+'): + result_expr = result_expr[1:] + + return result_expr, factor_with_exponent + +def solve_for_coefficients_optimized(k: int, r: int, i: int = 0) -> List[Tuple[str, Fraction]]: + """生成优化的a系数表达式""" + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs_optimized = [] + for j in range(k): + nonzero_items = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + nonzero_items.append((coeff, v_str)) + + if not nonzero_items: + a_coeffs_optimized.append(("0", Fraction(1, 1))) + continue + + expr_parts = [] + for coeff, v_str in nonzero_items: + frac = Fraction(coeff).limit_denominator(1000) + if coeff == 1.0: + expr_parts.append(f"+{v_str}") + elif coeff == -1.0: + expr_parts.append(f"-{v_str}") + else: + expr_parts.append(f"{frac}*{v_str}") + + expr = ''.join(expr_parts) + if expr.startswith('+'): + expr = expr[1:] + + optimized_expr, factor = parse_and_optimize_expression(expr, exponent=1) + a_coeffs_optimized.append((optimized_expr, factor)) + + return a_coeffs_optimized + +def integrate_polynomial_x(polynomial: Dict[int, List[Tuple[float, List[int]]]]) -> List[Tuple[Fraction, List[int]]]: + """对x在[-1/2, 1/2]上积分""" + a = Fraction(-1, 2) + b = Fraction(1, 2) + integrated_terms = [] + + for exp, expr_list in polynomial.items(): + numerator = b**(exp + 1) - a**(exp + 1) + integral_factor = Fraction(numerator, exp + 1) + + for coeff, symbols in expr_list: + coeff_frac = Fraction(str(coeff)) + new_coeff = coeff_frac * integral_factor + + if new_coeff != 0: + integrated_terms.append((new_coeff, symbols)) + + return integrated_terms + +def substitute_coefficients_optimized(integrated_terms: List[Tuple[Fraction, List[int]]], + a_coeffs_opt: List[Tuple[str, Fraction]]) -> Tuple[str, List]: + """代入并排序""" + result_dict = defaultdict(lambda: Fraction(0, 1)) + + for coeff, symbols in integrated_terms: + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + result_dict[symbol_key] += coeff + + # 构建带系数的项列表 + terms_with_coeffs = [] + + for symbol_key, total_coeff in result_dict.items(): + if total_coeff == 0: + continue + + base_symbol = symbol_key.split('*')[0].split('^')[0] + a_index = int(base_symbol[1:]) + + if a_index >= len(a_coeffs_opt): + raise ValueError(f"多项式索引{a_index}超出范围") + + a_expr, a_factor = a_coeffs_opt[a_index] + + is_squared = '^2' in symbol_key + exponent = 2 if is_squared else 1 + + final_coeff = total_coeff * (a_factor ** exponent) + coeff_abs = abs(final_coeff) + + coeff_str = format_coefficient(final_coeff) + + # 构建最终项 + if is_squared: + term_str = f"{coeff_str}*({a_expr})^2" if coeff_str not in ["1", "-1"] else \ + (f"({a_expr})^2" if coeff_str == "1" else f"-({a_expr})^2") + else: + term_str = f"{coeff_str}*{a_expr}" if coeff_str not in ["1", "-1"] else \ + (f"{a_expr}" if coeff_str == "1" else f"-{a_expr}") + + terms_with_coeffs.append((coeff_abs, final_coeff, term_str)) + + # ✅ 按系数绝对值降序排序 + terms_with_coeffs.sort(key=lambda x: x[0], reverse=True) + + # ✅ 提取排序后的项 + final_terms = [term_str for _, _, term_str in terms_with_coeffs] + + return smart_join_terms(final_terms), terms_with_coeffs + +def smart_join_terms(terms: List[str]) -> str: + """智能连接各项""" + if not terms: + return "0" + + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +# ============ 核心修改:两项表达式优化 ============ + +def optimize_two_term_expression(expr: str, is_latex: bool = False) -> str: + """ + ✅ 核心函数:优化两项表达式 + + 规则: + 1. 仅在两项时生效 + 2. -v[i-1]+v[i+1] -> v[i-1]-v[i+1](首项为正,保持顺序) + 3. 下标顺序:i-1在i+1前面 + + 参数: + expr: 括号内表达式,如 "-v[i-1]+v[i+1]" + is_latex: 是否为LaTeX格式 + + 返回: + 优化后表达式 + """ + # 检查是否为两项 + var_pattern = r'v_\{i[+-]\d*\}' if is_latex else r'v\[i[+-]?\d*\]' + if len(re.findall(var_pattern, expr)) != 2: + return expr + + # 匹配项:符号、系数、下标 + if is_latex: + pattern = r'([+-]?)(\d*)?v_\{i([+-]\d*)\}' + else: + pattern = r'([+-]?)(\d*\*)?v\[i([+-]\d*)\]' + + matches = re.findall(pattern, expr) + + if len(matches) != 2: + return expr + + # 解析两项 + terms = [] + for sign, coeff_part, index in matches: + if not sign: + sign = '+' + + if is_latex: + coeff = coeff_part if coeff_part else '1' + else: + coeff = coeff_part.rstrip('*') if coeff_part else '1' + + terms.append((sign, coeff, index)) + + # 检查首项是否为负 + if terms[0][0] == '-': + sign1, coeff1, idx1 = terms[0] # -v[i-1] + sign2, coeff2, idx2 = terms[1] # +v[i+1] + + # ✅ 保持顺序:i-1在i+1前面 + # -v[i-1] + v[i+1] -> v[i-1] - v[i+1] + new_parts = [] + + # 首项(原第一项,变号) + if is_latex: + if coeff1 == '1': + new_parts.append(f"v_{{i{idx1}}}") + else: + new_parts.append(f"{coeff1}v_{{i{idx1}}}") + else: + if coeff1 == '1': + new_parts.append(f"v[i{idx1}]") + else: + new_parts.append(f"{coeff1}*v[i{idx1}]") + + # 次项(原第二项,符号取反) + if is_latex: + if coeff2 == '1': + new_parts.append(f"-v_{{i{idx2}}}") + else: + new_parts.append(f"-{coeff2}v_{{i{idx2}}}") + else: + if coeff2 == '1': + new_parts.append(f"-v[i{idx2}]") + else: + new_parts.append(f"-{coeff2}*v[i{idx2}]") + + return ''.join(new_parts) + + return expr + +def apply_two_term_optimization_to_python(term_str: str) -> str: + """ + ✅ 对Python表达式应用两项优化 + + 示例: + "1/4*(-v[i-1]+v[i+1])^2" -> "1/4*(v[i-1]-v[i+1])^2" + """ + if '*(' not in term_str or '^2' not in term_str: + return term_str + + try: + coeff_part, main_part = term_str.split('*', 1) + main_part = main_part.strip() + + match = re.match(r'^\((.+)\)\^2$', main_part) + if not match: + return term_str + + inner_expr = match.group(1) + + # ✅ 应用两项优化(保持顺序) + optimized_inner = optimize_two_term_expression(inner_expr, is_latex=False) + + if optimized_inner != inner_expr: + term_str = f"{coeff_part}*({optimized_inner})^2" + + except: + pass + + return term_str + +def apply_two_term_optimization_to_latex(term_str: str, latex_coeff: str) -> str: + """ + ✅ 对LaTeX表达式应用两项优化 + + 示例: + "\\cfrac{1}{4}(-v_{i-1}+v_{i+1})^2" -> "\\cfrac{1}{4}(v_{i-1}-v_{i+1})^2" + """ + # 提取括号部分 + match = re.search(r'\((.+)\)\^2', term_str) + if not match: + return term_str + + inner_expr = match.group(1) + + # ✅ 应用两项优化(保持顺序) + optimized_inner = optimize_two_term_expression(inner_expr, is_latex=True) + + if optimized_inner != inner_expr: + term_str = term_str.replace(f"({inner_expr})^2", f"({optimized_inner})^2") + + return term_str + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + k: int, r: int, i: int = 0) -> Tuple[str, List]: + """完整流程:积分 → 代入""" + a_coeffs_opt = solve_for_coefficients_optimized(k, r, i) + integrated_terms = integrate_polynomial_x(polynomial) + return substitute_coefficients_optimized(integrated_terms, a_coeffs_opt) + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式β_r(完整优化版)""" + f_r_dict = {} + all_terms_info = {} + + print(f"\n生成k={k}的复合表达式β_r") + print("="*70) + + for r in range(k): + final_expr, terms_with_coeffs = evaluate_polynomial_integral_symbolic(polynomial, k, r, i) + f_r_dict[r] = final_expr + all_terms_info[r] = terms_with_coeffs + + print(f"\nβ_{r} = {final_expr}") + + # ============ LaTeX格式总结 ============ + print("\n" + "="*70) + print("LaTeX格式总结(按系数绝对值排序)") + print("="*70) + + latex_dict = {} + for r, terms_info in all_terms_info.items(): + latex_parts = [] + for coeff_abs, final_coeff, term_str in terms_info: + latex_coeff = format_coefficient(final_coeff, latex_mode=True) + + # ✅ 转换为LaTeX并应用两项优化 + latex_main = apply_two_term_optimization_to_latex(term_str, latex_coeff) + + # ✅ 构建完整LaTeX项 + if latex_coeff == "1": + latex_term = latex_main + elif latex_coeff == "-1": + latex_term = f"-{latex_main}" + else: + latex_term = f"{latex_coeff}{latex_main}" + + latex_parts.append(latex_term) + + latex_expr = " + ".join(latex_parts) + latex_expr = latex_expr.replace("+ -", "- ") + latex_dict[r] = latex_expr + + print(f"\n$\\beta_{r} = {latex_expr}$") + + # ============ 最终汇总(单行格式 - 应用两项优化) ============ + print("\n" + "="*70) + print("最终汇总(单行格式)") + print("="*70) + + python_dict = {} + for r, terms_info in all_terms_info.items(): + python_parts = [] + for coeff_abs, final_coeff, term_str in terms_info: + # ✅ 应用两项优化到Python表达式 + optimized_term = apply_two_term_optimization_to_python(term_str) + python_parts.append(optimized_term) + + python_expr = " + ".join(python_parts).replace("+ -", "- ") + python_dict[r] = python_expr + + print(f"β{r} = {python_expr}") + + # ============ LaTeX代码块 ============ + print("\n" + "="*70) + print("LaTeX代码块(统一在array环境中)") + print("="*70) + print("\n```latex") + print("\\begin{array}{l}") + for r in range(k): + if r == k - 1: + print(f" \\beta_{r} = {latex_dict[r]}") + else: + print(f" \\beta_{r} = {latex_dict[r]}\\\\") + print("\\end{array}") + print("```") + + return f_r_dict + +def print_expression_pretty(expr: str, indent: str = "", single_line: bool = False): + """支持单行输出""" + if single_line: + print(f"{indent}{expr}") + return + + if '+' not in expr: + print(f"{indent}{expr}") + return + + lines = [] + bracket_depth = 0 + current = "" + + for char in expr: + current += char + if char == '(': + bracket_depth += 1 + elif char == ')': + bracket_depth -= 1 + elif char == '+' and bracket_depth == 0: + lines.append(current[:-1].strip()) + current = "" + + if current: + lines.append(current.strip()) + + for i, line in enumerate(lines): + if line.startswith('-'): + print(f"{indent}{line}") + elif i == 0: + print(f"{indent}{line}") + else: + print(f"{indent}+ {line}") + +def test_composite(): + """测试例子""" + k = 3 + + polynomial = { + 0: [(1.0, [1, 1]), (4.0, [2, 2])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(完整优化版)") + print("="*70) + + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/03/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/03/compute_integral.py new file mode 100644 index 00000000..13bd8bd7 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/03/compute_integral.py @@ -0,0 +1,544 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +from math import gcd +from functools import reduce +import re + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def format_coefficient(frac: Fraction, latex_mode: bool = False) -> str: + """ + 智能格式化Fraction为字符串 + 参数: + latex_mode: 是否使用LaTeX格式(\\cfrac) + """ + if frac == 0: + return "0" + if frac.denominator == 1: + return str(int(frac.numerator)) + + if latex_mode: + return f"\\cfrac{{{frac.numerator}}}{{{frac.denominator}}}" + + return f"{frac.numerator}/{frac.denominator}" + +def compute_lcm(numbers: List[int]) -> int: + """计算最小公倍数""" + if not numbers: + return 1 + return reduce(lambda a, b: a * b // gcd(a, b), numbers) + +def parse_and_optimize_expression(expr: str, exponent: int = 1) -> Tuple[str, Fraction]: + """使用正则表达式安全解析表达式""" + if expr == "0": + return "0", Fraction(1, 1) + + if expr[0] not in '+-': + expr = '+' + expr + + pattern = r'([+-])?(?:(\d+/\d+|\d+|[+-]?\d+/\d+|[+-]?\d+)\*)?(v\[i[+-]?\d*\]|v\[i\])' + matches = re.findall(pattern, expr) + + coeff_vars = [] + denominators = [] + + for match in matches: + sign, coeff_part, var = match + + if coeff_part: + coeff_str = coeff_part.replace('+', '').replace('-', '') + if '/' in coeff_str: + num, den = map(int, coeff_str.split('/')) + coeff = Fraction(num, den) + else: + coeff = Fraction(int(coeff_str), 1) + else: + coeff = Fraction(1, 1) + + if sign == '-': + coeff = -coeff + + coeff_vars.append((coeff, var)) + + if coeff.denominator != 1: + denominators.append(coeff.denominator) + + if not coeff_vars: + return expr, Fraction(1, 1) + + lcm = compute_lcm(denominators) if denominators else 1 + + int_coeffs = [] + for coeff, var in coeff_vars: + int_coeff = coeff * lcm + int_coeffs.append((int(int_coeff.numerator), var)) + + neg_count = sum(1 for c, _ in int_coeffs if c < 0) + pos_count = sum(1 for c, _ in int_coeffs if c > 0) + + factor = Fraction(1, lcm) + if neg_count > pos_count: + factor = -factor + int_coeffs = [(-c, v) for c, v in int_coeffs] + + factor_with_exponent = factor ** exponent + + expr_parts = [] + for coeff, var in int_coeffs: + if coeff == 1: + expr_parts.append(f"+{var}") + elif coeff == -1: + expr_parts.append(f"-{var}") + elif coeff > 0: + expr_parts.append(f"+{coeff}*{var}") + else: + expr_parts.append(f"{coeff}*{var}") + + result_expr = ''.join(expr_parts) + if result_expr.startswith('+'): + result_expr = result_expr[1:] + + return result_expr, factor_with_exponent + +def solve_for_coefficients_optimized(k: int, r: int, i: int = 0) -> List[Tuple[str, Fraction]]: + """生成优化的a系数表达式""" + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs_optimized = [] + for j in range(k): + nonzero_items = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + nonzero_items.append((coeff, v_str)) + + if not nonzero_items: + a_coeffs_optimized.append(("0", Fraction(1, 1))) + continue + + expr_parts = [] + for coeff, v_str in nonzero_items: + frac = Fraction(coeff).limit_denominator(1000) + if coeff == 1.0: + expr_parts.append(f"+{v_str}") + elif coeff == -1.0: + expr_parts.append(f"-{v_str}") + else: + expr_parts.append(f"{frac}*{v_str}") + + expr = ''.join(expr_parts) + if expr.startswith('+'): + expr = expr[1:] + + optimized_expr, factor = parse_and_optimize_expression(expr, exponent=1) + a_coeffs_optimized.append((optimized_expr, factor)) + + return a_coeffs_optimized + +def integrate_polynomial_x(polynomial: Dict[int, List[Tuple[float, List[int]]]]) -> List[Tuple[Fraction, List[int]]]: + """对x在[-1/2, 1/2]上积分""" + a = Fraction(-1, 2) + b = Fraction(1, 2) + integrated_terms = [] + + for exp, expr_list in polynomial.items(): + numerator = b**(exp + 1) - a**(exp + 1) + integral_factor = Fraction(numerator, exp + 1) + print(f'exp={exp},expr_list={expr_list}') + + for coeff, symbols in expr_list: + coeff_frac = Fraction(str(coeff)) + new_coeff = coeff_frac * integral_factor + + if new_coeff != 0: + integrated_terms.append((new_coeff, symbols)) + + return integrated_terms + +def substitute_coefficients_optimized(integrated_terms: List[Tuple[Fraction, List[int]]], + a_coeffs_opt: List[Tuple[str, Fraction]]) -> Tuple[str, List]: + """代入并排序""" + result_dict = defaultdict(lambda: Fraction(0, 1)) + + for coeff, symbols in integrated_terms: + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + result_dict[symbol_key] += coeff + + # 构建带系数的项列表 + terms_with_coeffs = [] + + for symbol_key, total_coeff in result_dict.items(): + if total_coeff == 0: + continue + + base_symbol = symbol_key.split('*')[0].split('^')[0] + a_index = int(base_symbol[1:]) + + if a_index >= len(a_coeffs_opt): + raise ValueError(f"多项式索引{a_index}超出范围") + + a_expr, a_factor = a_coeffs_opt[a_index] + + is_squared = '^2' in symbol_key + exponent = 2 if is_squared else 1 + + final_coeff = total_coeff * (a_factor ** exponent) + coeff_abs = abs(final_coeff) + + coeff_str = format_coefficient(final_coeff) + + # 构建最终项 + if is_squared: + term_str = f"{coeff_str}*({a_expr})^2" if coeff_str not in ["1", "-1"] else \ + (f"({a_expr})^2" if coeff_str == "1" else f"-({a_expr})^2") + else: + term_str = f"{coeff_str}*{a_expr}" if coeff_str not in ["1", "-1"] else \ + (f"{a_expr}" if coeff_str == "1" else f"-{a_expr}") + + terms_with_coeffs.append((coeff_abs, final_coeff, term_str)) + + # ✅ 按系数绝对值降序排序 + terms_with_coeffs.sort(key=lambda x: x[0], reverse=True) + + # ✅ 提取排序后的项 + final_terms = [term_str for _, _, term_str in terms_with_coeffs] + + return smart_join_terms(final_terms), terms_with_coeffs + +def smart_join_terms(terms: List[str]) -> str: + """智能连接各项""" + if not terms: + return "0" + + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +# ============ 核心修改:两项表达式优化 ============ + +def optimize_two_term_expression(expr: str, is_latex: bool = False) -> str: + """ + ✅ 核心函数:优化两项表达式 + + 规则: + 1. 仅在两项时生效 + 2. -v[i-1]+v[i+1] -> v[i-1]-v[i+1](首项为正,保持顺序) + 3. 下标顺序:i-1在i+1前面 + + 参数: + expr: 括号内表达式,如 "-v[i-1]+v[i+1]" + is_latex: 是否为LaTeX格式 + + 返回: + 优化后表达式 + """ + # 检查是否为两项 + var_pattern = r'v_\{i[+-]\d*\}' if is_latex else r'v\[i[+-]?\d*\]' + if len(re.findall(var_pattern, expr)) != 2: + return expr + + # 匹配项:符号、系数、下标 + if is_latex: + pattern = r'([+-]?)(\d*)?v_\{i([+-]\d*)\}' + else: + pattern = r'([+-]?)(\d*\*)?v\[i([+-]\d*)\]' + + matches = re.findall(pattern, expr) + + if len(matches) != 2: + return expr + + # 解析两项 + terms = [] + for sign, coeff_part, index in matches: + if not sign: + sign = '+' + + if is_latex: + coeff = coeff_part if coeff_part else '1' + else: + coeff = coeff_part.rstrip('*') if coeff_part else '1' + + terms.append((sign, coeff, index)) + + # 检查首项是否为负 + if terms[0][0] == '-': + sign1, coeff1, idx1 = terms[0] # -v[i-1] + sign2, coeff2, idx2 = terms[1] # +v[i+1] + + # ✅ 保持顺序:i-1在i+1前面 + # -v[i-1] + v[i+1] -> v[i-1] - v[i+1] + new_parts = [] + + # 首项(原第一项,变号) + if is_latex: + if coeff1 == '1': + new_parts.append(f"v_{{i{idx1}}}") + else: + new_parts.append(f"{coeff1}v_{{i{idx1}}}") + else: + if coeff1 == '1': + new_parts.append(f"v[i{idx1}]") + else: + new_parts.append(f"{coeff1}*v[i{idx1}]") + + # 次项(原第二项,符号取反) + if is_latex: + if coeff2 == '1': + new_parts.append(f"-v_{{i{idx2}}}") + else: + new_parts.append(f"-{coeff2}v_{{i{idx2}}}") + else: + if coeff2 == '1': + new_parts.append(f"-v[i{idx2}]") + else: + new_parts.append(f"-{coeff2}*v[i{idx2}]") + + return ''.join(new_parts) + + return expr + +def apply_two_term_optimization_to_python(term_str: str) -> str: + """ + ✅ 对Python表达式应用两项优化 + + 示例: + "1/4*(-v[i-1]+v[i+1])^2" -> "1/4*(v[i-1]-v[i+1])^2" + """ + if '*(' not in term_str or '^2' not in term_str: + return term_str + + try: + coeff_part, main_part = term_str.split('*', 1) + main_part = main_part.strip() + + match = re.match(r'^\((.+)\)\^2$', main_part) + if not match: + return term_str + + inner_expr = match.group(1) + + # ✅ 应用两项优化(保持顺序) + optimized_inner = optimize_two_term_expression(inner_expr, is_latex=False) + + if optimized_inner != inner_expr: + term_str = f"{coeff_part}*({optimized_inner})^2" + + except: + pass + + return term_str + +def apply_two_term_optimization_to_latex(term_str: str, latex_coeff: str) -> str: + """ + ✅ 对LaTeX表达式应用两项优化 + + 示例: + "\\cfrac{1}{4}(-v_{i-1}+v_{i+1})^2" -> "\\cfrac{1}{4}(v_{i-1}-v_{i+1})^2" + """ + # 提取括号部分 + match = re.search(r'\((.+)\)\^2', term_str) + if not match: + return term_str + + inner_expr = match.group(1) + + # ✅ 应用两项优化(保持顺序) + optimized_inner = optimize_two_term_expression(inner_expr, is_latex=True) + + if optimized_inner != inner_expr: + term_str = term_str.replace(f"({inner_expr})^2", f"({optimized_inner})^2") + + return term_str + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + k: int, r: int, i: int = 0) -> Tuple[str, List]: + """完整流程:积分 → 代入""" + a_coeffs_opt = solve_for_coefficients_optimized(k, r, i) + print(f'a_coeffs_opt={a_coeffs_opt}') + integrated_terms = integrate_polynomial_x(polynomial) + print(f'integrated_terms={integrated_terms}') + return substitute_coefficients_optimized(integrated_terms, a_coeffs_opt) + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式β_r(完整优化版)""" + f_r_dict = {} + all_terms_info = {} + + print(f"\n生成k={k}的复合表达式β_r") + print("="*70) + + for r in range(k): + final_expr, terms_with_coeffs = evaluate_polynomial_integral_symbolic(polynomial, k, r, i) + f_r_dict[r] = final_expr + all_terms_info[r] = terms_with_coeffs + + print(f"\nβ_{r} = {final_expr}") + + # ============ LaTeX格式总结 ============ + print("\n" + "="*70) + print("LaTeX格式总结(按系数绝对值排序)") + print("="*70) + + latex_dict = {} + for r, terms_info in all_terms_info.items(): + latex_parts = [] + for coeff_abs, final_coeff, term_str in terms_info: + latex_coeff = format_coefficient(final_coeff, latex_mode=True) + + # ✅ 转换为LaTeX并应用两项优化 + latex_main = apply_two_term_optimization_to_latex(term_str, latex_coeff) + + # ✅ 构建完整LaTeX项 + if latex_coeff == "1": + latex_term = latex_main + elif latex_coeff == "-1": + latex_term = f"-{latex_main}" + else: + latex_term = f"{latex_coeff}{latex_main}" + + latex_parts.append(latex_term) + + latex_expr = " + ".join(latex_parts) + latex_expr = latex_expr.replace("+ -", "- ") + latex_dict[r] = latex_expr + + print(f"\n$\\beta_{r} = {latex_expr}$") + + # ============ 最终汇总(单行格式 - 应用两项优化) ============ + print("\n" + "="*70) + print("最终汇总(单行格式)") + print("="*70) + + python_dict = {} + for r, terms_info in all_terms_info.items(): + python_parts = [] + for coeff_abs, final_coeff, term_str in terms_info: + # ✅ 应用两项优化到Python表达式 + optimized_term = apply_two_term_optimization_to_python(term_str) + python_parts.append(optimized_term) + + python_expr = " + ".join(python_parts).replace("+ -", "- ") + python_dict[r] = python_expr + + print(f"β{r} = {python_expr}") + + # ============ LaTeX代码块 ============ + print("\n" + "="*70) + print("LaTeX代码块(统一在array环境中)") + print("="*70) + print("\n```latex") + print("\\begin{array}{l}") + for r in range(k): + if r == k - 1: + print(f" \\beta_{r} = {latex_dict[r]}") + else: + print(f" \\beta_{r} = {latex_dict[r]}\\\\") + print("\\end{array}") + print("```") + + return f_r_dict + +def print_expression_pretty(expr: str, indent: str = "", single_line: bool = False): + """支持单行输出""" + if single_line: + print(f"{indent}{expr}") + return + + if '+' not in expr: + print(f"{indent}{expr}") + return + + lines = [] + bracket_depth = 0 + current = "" + + for char in expr: + current += char + if char == '(': + bracket_depth += 1 + elif char == ')': + bracket_depth -= 1 + elif char == '+' and bracket_depth == 0: + lines.append(current[:-1].strip()) + current = "" + + if current: + lines.append(current.strip()) + + for i, line in enumerate(lines): + if line.startswith('-'): + print(f"{indent}{line}") + elif i == 0: + print(f"{indent}{line}") + else: + print(f"{indent}+ {line}") + +def test_composite(): + """测试例子""" + k = 3 + + polynomial = { + 0: [(1.0, [1, 1]), (4.0, [2, 2])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(完整优化版)") + print("="*70) + + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/counter/01/counter.py b/example/figure/1d/weno/interplate/counter/01/counter.py new file mode 100644 index 00000000..9e6ceaa4 --- /dev/null +++ b/example/figure/1d/weno/interplate/counter/01/counter.py @@ -0,0 +1,20 @@ +from collections import Counter + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +# 使用示例 +index_list = [0, 1, 3, 2, 5, 1] +indices, counts = sort_indices_with_counts(index_list) + +print(f"原始列表: {index_list}") +print(f"排序下标: {indices}") # [0, 1, 2, 3, 5] +print(f"出现次数: {counts}") # [1, 2, 1, 1, 1] \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/max_common_factor/01/max_common_factor.py b/example/figure/1d/weno/interplate/max_common_factor/01/max_common_factor.py new file mode 100644 index 00000000..58da9b12 --- /dev/null +++ b/example/figure/1d/weno/interplate/max_common_factor/01/max_common_factor.py @@ -0,0 +1,90 @@ +from fractions import Fraction +from math import gcd +from functools import reduce + +def extract_max_common_factor(numbers): + """ + 从数值列表中提取最大的公共因子,使得剩余部分为互质整数列表 + + 参数: + numbers: 数值列表(可包含整数、浮点数、字符串分数等) + + 返回: + tuple: (factor, simplified_list) + factor: Fraction类型,提取的公共因子 + simplified_list: 整数列表,最简形式(gcd=1) + """ + # 1. 将所有输入转换为Fraction,确保精确的有理数运算 + # Fraction(0.5) = 1/2, Fraction(-1) = -1/1, Fraction("1/3") = 1/3 + fractions = [Fraction(x) for x in numbers] + + # 2. 特殊情况处理 + if not fractions: # 空列表 + return Fraction(1, 1), [] + + if all(f == 0 for f in fractions): # 全零列表 + return Fraction(1, 1), [0] * len(fractions) + + # 3. 提取所有分子和分母 + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + # 4. 计算分子的最大公约数(GCD) + # reduce函数连续应用gcd: gcd(gcd(p1,p2), p3), ... + numerator_gcd = reduce(gcd, numerators) + + # 5. 计算分母的最小公倍数(LCM) + def lcm(a, b): + """计算两个数的最小公倍数:lcm(a,b) = |a×b| / gcd(a,b)""" + if a == 0 or b == 0: + return 0 + return abs(a * b) // gcd(a, b) + + # 连续应用lcm: lcm(lcm(q1,q2), q3), ... + denominator_lcm = reduce(lcm, denominators) + + # 6. 最大公共因子 = 分子GCD / 分母LCM + common_factor = Fraction(numerator_gcd, denominator_lcm) + + # 7. 计算简化后的列表 + simplified_fractions = [f / common_factor for f in fractions] + + # 8. 验证并转换为整数列表 + # 理论上所有分母都应为1,因为除以了最大公共因子 + simplified_integers = [sf.numerator for sf in simplified_fractions] + + # 9. 额外验证:确保结果整数列表互质(gcd=1) + verification_gcd = reduce(gcd, simplified_integers) + + # 如果verification_gcd≠1,说明还能继续提取,调整结果 + if verification_gcd != 1: + true_factor = common_factor * verification_gcd + simplified_integers = [x // verification_gcd for x in simplified_integers] + return true_factor, simplified_integers + + return common_factor, simplified_integers + + +# 测试函数 +def demonstrate(): + """演示各种情况""" + test_cases = [ + ("用户示例", [Fraction(1,2), -1, Fraction(1,2)]), + ("整数提取", [2, -4, 2]), + ("分数列表", [Fraction(1,3), Fraction(2,3), -1]), + ("浮点数", [0.5, -1.0, 0.5]), + ("互质整数", [1, 2, 3]), + ("负分数", [Fraction(-2,3), Fraction(4,3), -2]), + ] + + for name, case in test_cases: + factor, simplified = extract_max_common_factor(case) + print(f"【{name}】") + print(f" 原始列表: {case}") + print(f" 提取系数: {factor} (小数形式: {float(factor)})") + print(f" 简化列表: {simplified}") + print(f" 验证除法: {[Fraction(case[i]) / factor for i in range(len(case))]}") + print(f" 简化列表gcd: {reduce(gcd, simplified)}\n") + +# 运行演示 +demonstrate() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/01/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/01/polynomial_operations.py new file mode 100644 index 00000000..6fc5299c --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/01/polynomial_operations.py @@ -0,0 +1,225 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def format_expression(expr: Expression) -> str: + """格式化 Expression""" + if not expr: + return "0" + + term_strs = [format_term(term) for term in expr] + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +# ============= 测试 ============= + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + +# 运行测试 +test_polynomial_operations() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/01a/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/01a/polynomial_operations.py new file mode 100644 index 00000000..bfe47f27 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/01a/polynomial_operations.py @@ -0,0 +1,263 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def format_expression(expr: Expression) -> str: + """格式化 Expression""" + if not expr: + return "0" + + term_strs = [format_term(term) for term in expr] + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +# ============= 测试 ============= + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_verify_format(): + poly1 = {0: [(1, [1])], 1: [(2, [2])]} + + squared = polynomial_square(poly1) + # squared 仍然是 Polynomial 格式: + # { + # 0: [(1, [1, 1])], + # 1: [(4, [1, 2])], + # 2: [(4, [2, 2])] + # } + + integrated = integrate_polynomial(squared) + # integrated 仍然是 Polynomial 格式: + # { + # 1: [(1.0, [1, 1])], + # 2: [(2.0, [1, 2])], + # 3: [(1.333..., [2, 2])] + # } + + # 测试 + verify_format(poly1) # ✓ 通过 + verify_format(squared) # ✓ 通过 + verify_format(integrated) # ✓ 通过 + +# 运行测试 +#test_polynomial_operations() +test_verify_format() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/01b/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/01b/polynomial_operations.py new file mode 100644 index 00000000..b0177cd2 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/01b/polynomial_operations.py @@ -0,0 +1,441 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def format_expression(expr: Expression) -> str: + """格式化 Expression""" + if not expr: + return "0" + + term_strs = [format_term(term) for term in expr] + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def demo_smoothness_indicator_old(): + print(f'demo_smoothness_indicator') + + n = 5 + m = 2 + coeff, power = derivative_form(5, 2) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k-1 + cols = k-1 + matrix = np.empty((rows, cols), dtype=object) + #print(f'matrix=\n{matrix}') + + #x^1 x^2 x^3 + #d^1dx^1 1x^0 2x^1 3x^2 + #d^2dx^2 0x^0 2x^0 6x^1 + #d^3dx^3 0x^0 0x^0 6x^0 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j+1, i+1) + acoef = j + 1 + matrix[i][j] = (coef, acoef, power) + print(f"{coeff}x^{power}",end=' ') + print() + + print(f'matrix=\n{matrix}') + power_map_list = [] + for i in range(rows): + power_map = defaultdict(list) + for j in range(cols): + coef, acoef, power = matrix[i][j] + if coef != 0: + power_map[power].append((coef, acoef)) + print(f"{coef}*a{acoef}*x^{power}",end=' ') + power_map_list.append(power_map) + print() + + print(f'power_map_list={power_map_list}') + for i in range(rows): + print_power_symbol(power_map_list[i]) + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def demo_smoothness_indicator(): + print('demo_smoothness_indicator_new') + + n = 5 + m = 2 + coeff, power = derivative_form(n, m) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k - 1 + cols = k - 1 + + # 改造重点2:matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 第一阶段:构建matrix并打印导数系数表 + print("\n=== 导数系数表 (coef*x^power) ===") + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + print(f"{coef}x^{power}", end=' ') + print() + + print(f'\nmatrix=\n{matrix}') + + # 改造重点3:从matrix构建Polynomial List + print("\n=== 符号表达式表 (coef*a{acoef}*x^power) ===") + polynomial_list = [] + for i in range(rows): + # Polynomial = {指数: Expression} + polynomial = defaultdict(list) + + for j in range(cols): + term, power = matrix[i][j] + coef, symbols = term + + if coef != 0: + # 添加到对应指数的项列表 + polynomial[power].append(term) + print(f"{coef}*a{symbols[0]}*x^{power}", end=' ') + else: + print("0 ", end=' ') + + polynomial_list.append(dict(polynomial)) + print() + + print(f'\npolynomial_list={polynomial_list}') + + # 改造重点4:用新函数打印(保持旧格式) + print("\n=== 最终输出(使用新结构)===") + for i, poly in enumerate(polynomial_list): + print(f"-- 第{i+1}行导数 --") + print_polynomial_old_style(poly) + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_verify_format(): + poly1 = {0: [(1, [1])], 1: [(2, [2])]} + + squared = polynomial_square(poly1) + # squared 仍然是 Polynomial 格式: + # { + # 0: [(1, [1, 1])], + # 1: [(4, [1, 2])], + # 2: [(4, [2, 2])] + # } + + integrated = integrate_polynomial(squared) + # integrated 仍然是 Polynomial 格式: + # { + # 1: [(1.0, [1, 1])], + # 2: [(2.0, [1, 2])], + # 3: [(1.333..., [2, 2])] + # } + + # 测试 + verify_format(poly1) # ✓ 通过 + verify_format(squared) # ✓ 通过 + verify_format(integrated) # ✓ 通过 + +if __name__ == "__main__": + demo_smoothness_indicator() + #square_polynomial_test() + #test_verify_format() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/01c/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/01c/polynomial_operations.py new file mode 100644 index 00000000..4c7d945d --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/01c/polynomial_operations.py @@ -0,0 +1,436 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def format_expression(expr: Expression) -> str: + """格式化 Expression""" + if not expr: + return "0" + + term_strs = [format_term(term) for term in expr] + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def demo_smoothness_indicator_old(): + print(f'demo_smoothness_indicator') + + n = 5 + m = 2 + coeff, power = derivative_form(5, 2) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k-1 + cols = k-1 + matrix = np.empty((rows, cols), dtype=object) + #print(f'matrix=\n{matrix}') + + #x^1 x^2 x^3 + #d^1dx^1 1x^0 2x^1 3x^2 + #d^2dx^2 0x^0 2x^0 6x^1 + #d^3dx^3 0x^0 0x^0 6x^0 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j+1, i+1) + acoef = j + 1 + matrix[i][j] = (coef, acoef, power) + print(f"{coeff}x^{power}",end=' ') + print() + + print(f'matrix=\n{matrix}') + power_map_list = [] + for i in range(rows): + power_map = defaultdict(list) + for j in range(cols): + coef, acoef, power = matrix[i][j] + if coef != 0: + power_map[power].append((coef, acoef)) + print(f"{coef}*a{acoef}*x^{power}",end=' ') + power_map_list.append(power_map) + print() + + print(f'power_map_list={power_map_list}') + for i in range(rows): + print_power_symbol(power_map_list[i]) + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def demo_smoothness_indicator(): + k = 3 + rows = k - 1 + cols = k - 1 + + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + print("\n=== 导数系数表 (coef*x^power) ===") + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + print(f"{coef}x^{power}", end=' ') + print() + + print(f'\nmatrix=\n{matrix}') + + # 从matrix构建Polynomial List + print("\n=== 符号表达式表 (coef*a{acoef}*x^power) ===") + polynomial_list = [] + for i in range(rows): + # Polynomial = {指数: Expression} + polynomial = defaultdict(list) + + for j in range(cols): + term, power = matrix[i][j] + coef, symbols = term + + if coef != 0: + # 添加到对应指数的项列表 + polynomial[power].append(term) + print(f"{coef}*a{symbols[0]}*x^{power}", end=' ') + else: + print("0 ", end=' ') + + polynomial_list.append(dict(polynomial)) + print() + + print(f'\npolynomial_list={polynomial_list}') + + for i, poly in enumerate(polynomial_list): + print(f"-- 第{i+1}行导数 --") + print_polynomial(poly) + squared = polynomial_square(poly) + print_polynomial(squared, "平方展开后") + integrated = integrate_polynomial(squared) + print_polynomial(integrated, "积分后") + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_verify_format(): + poly1 = {0: [(1, [1])], 1: [(2, [2])]} + + squared = polynomial_square(poly1) + # squared 仍然是 Polynomial 格式: + # { + # 0: [(1, [1, 1])], + # 1: [(4, [1, 2])], + # 2: [(4, [2, 2])] + # } + + integrated = integrate_polynomial(squared) + # integrated 仍然是 Polynomial 格式: + # { + # 1: [(1.0, [1, 1])], + # 2: [(2.0, [1, 2])], + # 3: [(1.333..., [2, 2])] + # } + + # 测试 + verify_format(poly1) # ✓ 通过 + verify_format(squared) # ✓ 通过 + verify_format(integrated) # ✓ 通过 + +if __name__ == "__main__": + demo_smoothness_indicator() + #square_polynomial_test() + #test_verify_format() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/01d/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/01d/polynomial_operations.py new file mode 100644 index 00000000..685638d8 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/01d/polynomial_operations.py @@ -0,0 +1,599 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def demo_smoothness_indicator_old(): + print(f'demo_smoothness_indicator') + + n = 5 + m = 2 + coeff, power = derivative_form(5, 2) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k-1 + cols = k-1 + matrix = np.empty((rows, cols), dtype=object) + #print(f'matrix=\n{matrix}') + + #x^1 x^2 x^3 + #d^1dx^1 1x^0 2x^1 3x^2 + #d^2dx^2 0x^0 2x^0 6x^1 + #d^3dx^3 0x^0 0x^0 6x^0 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j+1, i+1) + acoef = j + 1 + matrix[i][j] = (coef, acoef, power) + print(f"{coeff}x^{power}",end=' ') + print() + + print(f'matrix=\n{matrix}') + power_map_list = [] + for i in range(rows): + power_map = defaultdict(list) + for j in range(cols): + coef, acoef, power = matrix[i][j] + if coef != 0: + power_map[power].append((coef, acoef)) + print(f"{coef}*a{acoef}*x^{power}",end=' ') + power_map_list.append(power_map) + print() + + print(f'power_map_list={power_map_list}') + for i in range(rows): + print_power_symbol(power_map_list[i]) + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def demo_smoothness_indicator(): + k = 3 + rows = k - 1 + cols = k - 1 + + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + print("\n=== 导数系数表 (coef*x^power) ===") + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + print(f"{coef}x^{power}", end=' ') + print() + + print(f'\nmatrix=\n{matrix}') + + # 从matrix构建Polynomial List + print("\n=== 符号表达式表 (coef*a{acoef}*x^power) ===") + polynomial_list = [] + for i in range(rows): + # Polynomial = {指数: Expression} + polynomial = defaultdict(list) + + for j in range(cols): + term, power = matrix[i][j] + coef, symbols = term + + if coef != 0: + # 添加到对应指数的项列表 + polynomial[power].append(term) + print(f"{coef}*a{symbols[0]}*x^{power}", end=' ') + else: + print("0 ", end=' ') + + polynomial_list.append(dict(polynomial)) + print() + + print(f'\npolynomial_list={polynomial_list}') + + for i, poly in enumerate(polynomial_list): + print(f"-- 第{i+1}行导数 --") + print_polynomial(poly) + squared = polynomial_square(poly) + print_polynomial(squared, "平方展开后") + integrated = integrate_polynomial(squared) + print_polynomial(integrated, "积分后") + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +if __name__ == "__main__": + #demo_smoothness_indicator() + test_same_bounds() diff --git a/example/figure/1d/weno/interplate/polynomial_operations/01e/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/01e/polynomial_operations.py new file mode 100644 index 00000000..9aac95ea --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/01e/polynomial_operations.py @@ -0,0 +1,564 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def demo_smoothness_indicator(): + k = 3 + rows = k - 1 + cols = k - 1 + + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + print(f'matrix=\n{matrix}') + + # 从matrix构建Polynomial List + polynomial_list = [] + for i in range(rows): + # Polynomial = {指数: Expression} + polynomial = defaultdict(list) + + for j in range(cols): + term, power = matrix[i][j] + coef, symbols = term + + if coef != 0: + # 添加到对应指数的项列表 + polynomial[power].append(term) + + polynomial_list.append(dict(polynomial)) + + print(f'\npolynomial_list={polynomial_list}') + + squared_list = [] + for i, poly in enumerate(polynomial_list): + #print(f"-- 第{i+1}行导数 --") + #print_polynomial(poly) + squared = polynomial_square(poly) + squared_list.append( squared ) + #print_polynomial(squared, "平方展开后") + + + print("\n原始多项式列表:") + for i, p in enumerate(squared_list, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(squared_list, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +if __name__ == "__main__": + #test_same_bounds() + demo_smoothness_indicator() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02/polynomial_operations.py new file mode 100644 index 00000000..94c62fd1 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02/polynomial_operations.py @@ -0,0 +1,598 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_derivate_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def demo_smoothness_indicator(k): + rows = k - 1 + cols = k - 1 + + matrix = create_derivate_matrix(k) + + print(f'matrix=\n{matrix}') + + # 从matrix构建Polynomial List + polynomial_list = [] + for i in range(rows): + # Polynomial = {指数: Expression} + polynomial = defaultdict(list) + + for j in range(cols): + term, power = matrix[i][j] + coef, symbols = term + + if coef != 0: + # 添加到对应指数的项列表 + polynomial[power].append(term) + + polynomial_list.append(dict(polynomial)) + + print(f'\npolynomial_list={polynomial_list}') + + squared_list = [] + for i, poly in enumerate(polynomial_list): + #print(f"-- 第{i+1}行导数 --") + #print_polynomial(poly) + squared = polynomial_square(poly) + squared_list.append( squared ) + #print_polynomial(squared, "平方展开后") + + + print("\n原始多项式列表:") + for i, p in enumerate(squared_list, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(squared_list, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + + print(f'total_result={total_result}') + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02a/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02a/polynomial_operations.py new file mode 100644 index 00000000..36fb75f0 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02a/polynomial_operations.py @@ -0,0 +1,708 @@ +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + v_str = f'{csign}{frac_str}*v[{id}]' + return v_str + +def print_coeffs_expression(a_coeffs): + rows, cols = a_coeffs.shape + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def demo_smoothness_indicator(k): + """ + 计算平滑性指标的演示函数。 + 步骤: + 1. 创建差分矩阵。 + 2. 从矩阵构建多项式列表。 + 3. 计算每个多项式的平方。 + 4. 打印平方后的多项式。 + 5. 在区间[-1/2, 1/2]上积分求和。 + 6. 输出最终结果。 + """ + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + print(f'total_result={total_result}') + + r = 1 + M = compute_mass_matrix(k,r) + print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + print_coeffs_expression(a_coeffs) + + + + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02b/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02b/polynomial_operations.py new file mode 100644 index 00000000..51d14040 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02b/polynomial_operations.py @@ -0,0 +1,717 @@ +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign}{frac_str}*v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def demo_smoothness_indicator(k): + """ + 计算平滑性指标的演示函数。 + 步骤: + 1. 创建差分矩阵。 + 2. 从矩阵构建多项式列表。 + 3. 计算每个多项式的平方。 + 4. 打印平方后的多项式。 + 5. 在区间[-1/2, 1/2]上积分求和。 + 6. 输出最终结果。 + """ + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + print(f'total_result={total_result}') + + for r in range(k): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + print_coeffs_expression(a_coeffs,k,r) + + + + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02c/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02c/polynomial_operations.py new file mode 100644 index 00000000..9dc4585c --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02c/polynomial_operations.py @@ -0,0 +1,796 @@ +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign}{frac_str}*v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def print_reconstruction_coefficients(k, r_values, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{k-1}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + # Generate index range string + def get_index_range(r): + if r == 0: + return f"[i, i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r}, i+{k-1-r}]" + + # Process each r value + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +#def solve_polynomial_coefficients(k) + +def demo_smoothness_indicator(k): + """ + 计算平滑性指标的演示函数。 + 步骤: + 1. 创建差分矩阵。 + 2. 从矩阵构建多项式列表。 + 3. 计算每个多项式的平方。 + 4. 打印平方后的多项式。 + 5. 在区间[-1/2, 1/2]上积分求和。 + 6. 输出最终结果。 + """ + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + #print_coeffs_expression(a_coeffs,k,r) + coeffs_list.append( a_coeffs ) + r_values = list(range(k)) + print_reconstruction_coefficients(k, r_values, coeffs_list) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02d/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02d/polynomial_operations.py new file mode 100644 index 00000000..eb029fbd --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02d/polynomial_operations.py @@ -0,0 +1,805 @@ +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign}{frac_str}*v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{k-1}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + #print(f"squared_polynomials={squared_polynomials}") + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + #print(f'total_result={total_result}') + return total_result + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print(f"expression = {format_expression(expression)}") + print(f"β{r} = {format_expression(expression)}") + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print(f'a_coeffs={a_coeffs}') + print_coeffs_expression(a_coeffs,k,r) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02e/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02e/polynomial_operations.py new file mode 100644 index 00000000..70ee4a99 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02e/polynomial_operations.py @@ -0,0 +1,859 @@ +from fractions import Fraction +from collections import Counter +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign}{frac_str}*v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{k-1}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + #print(f"squared_polynomials={squared_polynomials}") + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + #print(f'total_result={total_result}') + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "" + + term_strs = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + for i in range(nSize): + id = indices[i] + co = counts[i] + symbol_str.append(f"a[{id}]^{co}") + + symbol_str_final = "*".join(symbol_str) + print(f"symbol_str_final: {symbol_str_final}") + term_strs.append(f"{coeff}*{symbol_str_final}") + + return " + ".join(term_strs) + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print(f"expression = {format_expression(expression)}") + print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"expr_str = {expr_str}") + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print(f'a_coeffs={a_coeffs}') + print_coeffs_expression(a_coeffs,k,r) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02f/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02f/polynomial_operations.py new file mode 100644 index 00000000..c7fec81b --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02f/polynomial_operations.py @@ -0,0 +1,870 @@ +from fractions import Fraction +from collections import Counter +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign}{frac_str}*v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{k-1}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + #print(f"squared_polynomials={squared_polynomials}") + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + #print(f'total_result={total_result}') + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ''.join(expr_parts) + print(f'{expr}') + return expr + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "" + + term_strs = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + for i in range(nSize): + id = indices[i] + co = counts[i] + coefficients_str = polynomial_coefficients_str(a_coeffs[id],k,r) + symbol_str.append(f"({coefficients_str})^{co}") + + symbol_str_final = "*".join(symbol_str) + print(f"symbol_str_final: {symbol_str_final}") + term_strs.append(f"{coeff}*{symbol_str_final}") + + return " + ".join(term_strs) + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print(f"expression = {format_expression(expression)}") + print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"expr_str = {expr_str}") + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + #print(f'a_coeffs={a_coeffs}') + #print_coeffs_expression(a_coeffs,k,r) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/03/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/03/polynomial_operations.py new file mode 100644 index 00000000..6a62554e --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/03/polynomial_operations.py @@ -0,0 +1,1124 @@ +from fractions import Fraction +from collections import Counter +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factorBAK(numbers): + """ + 从数值列表中提取最大的公共因子,使得剩余部分为互质整数列表 + + 参数: + numbers: 数值列表(可包含整数、浮点数、字符串分数等) + + 返回: + tuple: (factor, simplified_list) + factor: Fraction类型,提取的公共因子 + simplified_list: 整数列表,最简形式(gcd=1) + """ + # 1. 将所有输入转换为Fraction,确保精确的有理数运算 + # Fraction(0.5) = 1/2, Fraction(-1) = -1/1, Fraction("1/3") = 1/3 + print(f'numbers={numbers}') + fractions = [Fraction(x) for x in numbers] + + # 2. 特殊情况处理 + if not fractions: # 空列表 + return Fraction(1, 1), [] + + if all(f == 0 for f in fractions): # 全零列表 + return Fraction(1, 1), [0] * len(fractions) + + # 3. 提取所有分子和分母 + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + # 4. 计算分子的最大公约数(GCD) + # reduce函数连续应用gcd: gcd(gcd(p1,p2), p3), ... + numerator_gcd = reduce(gcd, numerators) + + # 5. 计算分母的最小公倍数(LCM) + def lcm(a, b): + """计算两个数的最小公倍数:lcm(a,b) = |a×b| / gcd(a,b)""" + if a == 0 or b == 0: + return 0 + return abs(a * b) // gcd(a, b) + + # 连续应用lcm: lcm(lcm(q1,q2), q3), ... + denominator_lcm = reduce(lcm, denominators) + + # 6. 最大公共因子 = 分子GCD / 分母LCM + common_factor = Fraction(numerator_gcd, denominator_lcm) + + # 7. 计算简化后的列表 + simplified_fractions = [f / common_factor for f in fractions] + + # 8. 验证并转换为整数列表 + # 理论上所有分母都应为1,因为除以了最大公共因子 + simplified_integers = [sf.numerator for sf in simplified_fractions] + + # 9. 额外验证:确保结果整数列表互质(gcd=1) + verification_gcd = reduce(gcd, simplified_integers) + + # 如果verification_gcd≠1,说明还能继续提取,调整结果 + if verification_gcd != 1: + true_factor = common_factor * verification_gcd + simplified_integers = [x // verification_gcd for x in simplified_integers] + return true_factor, simplified_integers + + return common_factor, simplified_integers + +def extract_max_common_factorBAK1(numbers): + """ + 从数值列表中提取最大的公共因子,使得剩余部分为互质整数列表 + + 参数: + numbers: 数值列表(支持int、float、numpy标量、字符串等) + + 返回: + tuple: (factor, simplified_list) + factor: Fraction类型,提取的公共因子 + simplified_list: 整数列表,最简形式(gcd=1) + """ + + def _to_python_number(x): + """ + 关键修复:将numpy标量转换为Python原生类型 + """ + if isinstance(x, (np.integer, np.floating, np.ndarray)): + # numpy标量转Python标量 + return x.item() + return x + + + print(f'numbers={numbers}') + + # 1. 转换前先处理numpy类型 + processed_numbers = [_to_python_number(x) for x in numbers] + + # 2. 转换为Fraction(现在安全了) + fractions = [Fraction(x) for x in processed_numbers] + print(f'fractions={fractions}') + + # 3. 后续逻辑不变 + if not fractions: + return Fraction(1, 1), [] + + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + # 提取分子和分母 + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + # 计算分子GCD + numerator_gcd = reduce(gcd, numerators) + + # 计算分母LCM + def lcm(a, b): + if a == 0 or b == 0: + return 0 + return abs(a * b) // gcd(a, b) + + denominator_lcm = reduce(lcm, denominators) + + # 最大公共因子 = 分子GCD / 分母LCM + common_factor = Fraction(numerator_gcd, denominator_lcm) + + # 简化列表 + simplified_fractions = [f / common_factor for f in fractions] + simplified_integers = [sf.numerator for sf in simplified_fractions] + + # 验证互质 + verification_gcd = reduce(gcd, simplified_integers) + if verification_gcd != 1: + true_factor = common_factor * verification_gcd + simplified_integers = [x // verification_gcd for x in simplified_integers] + return true_factor, simplified_integers + + return common_factor, simplified_integers + +def extract_max_common_factor(numbers, max_denominator=1000000, tolerance=1e-12): + """ + 提取最大公共因子,支持numpy浮点数精度修复 + + 参数: + numbers: 数值列表(支持numpy标量、float、int等) + max_denominator: 限制分母的最大值,用于修复浮点误差 + tolerance: 接近零值的容差阈值 + """ + + def _to_python_number(x): + """转换numpy标量为Python原生类型""" + if isinstance(x, (np.integer, np.floating)): + return x.item() + if isinstance(x, np.ndarray) and x.size == 1: + return x.item() + return x + + def _smart_fraction(x): + """智能转换为Fraction,自动修复精度问题""" + # 1. 转换为Python原生数值 + val = _to_python_number(x) + + # 2. 处理接近零的值 + if abs(val) < tolerance: + return Fraction(0, 1) + + # 3. 对浮点数先限制分母再转换 + if isinstance(val, float): + # 先创建Fraction,再限制分母复杂度 + return Fraction(val).limit_denominator(max_denominator) + + # 4. 其他类型直接转换 + return Fraction(val) + + # 主逻辑 + fractions = [_smart_fraction(x) for x in numbers] + + if not fractions: + return Fraction(1, 1), [] + + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + # 计算分子GCD和分母LCM + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + + def lcm(a, b): + if a == 0 or b == 0: + return 0 + return abs(a * b) // gcd(a, b) + + denominator_lcm = reduce(lcm, denominators) + + # 最大公共因子 = 分子GCD / 分母LCM + common_factor = Fraction(numerator_gcd, denominator_lcm) + + # 简化并验证互质 + simplified = [f / common_factor for f in fractions] + simplified_integers = [sf.numerator for sf in simplified] + + # 确保gcd=1 + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + common_factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return common_factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign}{frac_str}*v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{k-1}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + #print(f"squared_polynomials={squared_polynomials}") + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + #print(f'total_result={total_result}') + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ''.join(expr_parts) + print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "" + + print(f"expr={expr}") + term_strs = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + print(f'counts={counts}') + for i in range(nSize): + id = indices[i] + co = counts[i] + #a_coeff = get_numeric_list(a_coeffs[id]) + print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + symbol_str.append(f"({coefficients_str})^{co}") + symbol_str_final = "*".join(symbol_str) + + print(f"symbol_str_final: {symbol_str_final}") + term_strs.append(f"{coeff*factors}*{symbol_str_final}") + + return " + ".join(term_strs) + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print(f"expression = {format_expression(expression)}") + print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"expr_str = {expr_str}") + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + #print(f'a_coeffs={a_coeffs}') + #print_coeffs_expression(a_coeffs,k,r) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/03a/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/03a/polynomial_operations.py new file mode 100644 index 00000000..5f6c50fd --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/03a/polynomial_operations.py @@ -0,0 +1,1055 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factorBAK(numbers, max_denominator=1000000, tolerance=1e-12): + """ + 提取最大公共因子,支持numpy浮点数精度修复 + + 参数: + numbers: 数值列表(支持numpy标量、float、int等) + max_denominator: 限制分母的最大值,用于修复浮点误差 + tolerance: 接近零值的容差阈值 + """ + + def _to_python_number(x): + """转换numpy标量为Python原生类型""" + if isinstance(x, (np.integer, np.floating)): + return x.item() + if isinstance(x, np.ndarray) and x.size == 1: + return x.item() + return x + + def _smart_fraction(x): + """智能转换为Fraction,自动修复精度问题""" + # 1. 转换为Python原生数值 + val = _to_python_number(x) + + # 2. 处理接近零的值 + if abs(val) < tolerance: + return Fraction(0, 1) + + # 3. 对浮点数先限制分母再转换 + if isinstance(val, float): + # 先创建Fraction,再限制分母复杂度 + return Fraction(val).limit_denominator(max_denominator) + + # 4. 其他类型直接转换 + return Fraction(val) + + # 主逻辑 + fractions = [_smart_fraction(x) for x in numbers] + + if not fractions: + return Fraction(1, 1), [] + + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + # 计算分子GCD和分母LCM + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + + def lcm(a, b): + if a == 0 or b == 0: + return 0 + return abs(a * b) // gcd(a, b) + + denominator_lcm = reduce(lcm, denominators) + + # 最大公共因子 = 分子GCD / 分母LCM + common_factor = Fraction(numerator_gcd, denominator_lcm) + + # 简化并验证互质 + simplified = [f / common_factor for f in fractions] + simplified_integers = [sf.numerator for sf in simplified] + + # 确保gcd=1 + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + common_factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return common_factor, simplified_integers + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}*" + frac_star = f"{frac_str}·" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + #print(f"squared_polynomials={squared_polynomials}") + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + #print(f'total_result={total_result}') + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "" + + term_strs = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + #print(f'counts={counts}') + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000) + term_strs.append(f"{frac}·{symbol_str_final}") + + return " + ".join(term_strs) + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_smoothness_indicator(expression,a_coeffs,k,r): + #print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + #print(f'a_coeffs={a_coeffs}') + #print_coeffs_expression(a_coeffs,k,r) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/03b/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/03b/polynomial_operations.py new file mode 100644 index 00000000..098b2401 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/03b/polynomial_operations.py @@ -0,0 +1,893 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + #print(f"polynomials={polynomials}") + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + #print(f" Integration Result for Term{idx+1}: {format_expression(integral_result)}") + print(f" Integration Result for Term{idx+1}: {format_expression_fraction(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def format_expression_fraction(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + if len(symbols) == 1: + term_strs.append(f"{frac_str}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{frac_str}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{frac_str}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\nInitial Polynomial Expressions (before integration):") + for i, poly in enumerate(squared_polynomials, 1): + print(f" Polynomial Term {i}: P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def to_fraction(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + return frac + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}*" + frac_star = f"{frac_str}·" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + lower_bound = -0.5 + upper_bound = 0.5 + domain = f"[{to_fraction(lower_bound)}, {to_fraction(upper_bound)}]" + print(f"\nStep-by-step Integration and Summation (integration domain: x∈{domain}):") + + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\nFinal Aggregated Result (sum of all integrated terms):") + #formatted_result = format_expression(total_result) + formatted_result = format_expression_fraction(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "" + + term_strs = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + #print(f'counts={counts}') + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000) + term_strs.append(f"{frac}·{symbol_str_final}") + + return " + ".join(term_strs) + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_smoothness_indicator(expression,a_coeffs,k,r): + #print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def print_smoothness_indicators(expression,coeffs_list,k): + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + for r in range(k): + a_coeffs = coeffs_list[r] + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicatorOld(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + + print_smoothness_indicators(total_result,coeffs_list,k) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/03c/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/03c/polynomial_operations.py new file mode 100644 index 00000000..e7728cb9 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/03c/polynomial_operations.py @@ -0,0 +1,906 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + #print(f"polynomials={polynomials}") + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + #print(f" Integration Result for Term{idx+1}: {format_expression(integral_result)}") + print(f" Integration Result for Term{idx+1}: {format_expression_fraction(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def format_expression_fraction(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + if len(symbols) == 1: + term_strs.append(f"{frac_str}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{frac_str}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{frac_str}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\nInitial Polynomial Expressions (before integration):") + for i, poly in enumerate(squared_polynomials, 1): + print(f" Polynomial Term {i}: P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def to_fraction(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + return frac + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}·" + frac_star = f"{frac_str}" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + lower_bound = -0.5 + upper_bound = 0.5 + domain = f"[{to_fraction(lower_bound)}, {to_fraction(upper_bound)}]" + print(f"\nStep-by-step Integration and Summation (integration domain: x∈{domain}):") + + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\nFinal Aggregated Result (sum of all integrated terms):") + #formatted_result = format_expression(total_result) + formatted_result = format_expression_fraction(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def sort_by_first_list(primary_list, *other_lists, key=None, reverse=False): + """ + 根据第一个列表排序,同步调整任意数量其他列表 + + 参数: + primary_list: 主排序参考列表 + *other_lists: 其他需要同步排序的列表(可变参数) + key: 排序key函数(如abs, lambda x: x**2等) + reverse: 是否降序 + + 返回: + 元组: (sorted_primary, sorted_other1, sorted_other2, ...) + """ + # 核心:动态生成key函数 + if key is None: + key_func = lambda i: primary_list[i] # 默认:直接比较值 + else: + key_func = lambda i: key(primary_list[i]) # 自定义:对值应用key函数 + + # 获取排序索引 + indices = sorted(range(len(primary_list)), key=key_func, reverse=reverse) + + # 应用索引到所有列表(包括主列表) + all_lists = (primary_list,) + other_lists + result = tuple([lst[i] for i in indices] for lst in all_lists) + return result + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "" + + term_strs = [] + frac_list = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000) + frac_list.append(frac) + #term_strs.append(f"{frac}·{symbol_str_final}") + term_strs.append(f"{frac}{symbol_str_final}") + + _, term_strs = sort_by_first_list(frac_list, term_strs, key=abs, reverse=True) + + return " + ".join(term_strs) + +def print_smoothness_indicator(expression,a_coeffs,k,r): + #print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def print_smoothness_indicators(expression,coeffs_list,k): + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + for r in range(k): + a_coeffs = coeffs_list[r] + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicatorOld(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + + print_smoothness_indicators(total_result,coeffs_list,k) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/03d/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/03d/polynomial_operations.py new file mode 100644 index 00000000..51173fc4 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/03d/polynomial_operations.py @@ -0,0 +1,911 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + #print(f"sum_integrals_same_bounds polynomials={polynomials}") + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + #print(f" Integration Result for Term{idx+1}: {format_expression(integral_result)}") + print(f" Integration Result for Term{idx+1}: {format_expression_fraction(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def format_expression_fraction(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + if len(symbols) == 1: + term_strs.append(f"{frac_str}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{frac_str}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{frac_str}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\nInitial Polynomial Expressions (before integration):") + for i, poly in enumerate(squared_polynomials, 1): + print(f" Polynomial Term {i}: P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def to_fraction(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + return frac + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}·" + frac_star = f"{frac_str}" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f"k={k},polynomial_list={polynomial_list}") + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + lower_bound = -0.5 + upper_bound = 0.5 + domain = f"[{to_fraction(lower_bound)}, {to_fraction(upper_bound)}]" + print(f"\nStep-by-step Integration and Summation (integration domain: x∈{domain}):") + + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + #print(f"k={k},total_result={total_result}") + + # 打印最终结果 + print(f"\nFinal Aggregated Result (sum of all integrated terms):") + #formatted_result = format_expression(total_result) + formatted_result = format_expression_fraction(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def sort_by_first_list(primary_list, *other_lists, key=None, reverse=False): + """ + 根据第一个列表排序,同步调整任意数量其他列表 + + 参数: + primary_list: 主排序参考列表 + *other_lists: 其他需要同步排序的列表(可变参数) + key: 排序key函数(如abs, lambda x: x**2等) + reverse: 是否降序 + + 返回: + 元组: (sorted_primary, sorted_other1, sorted_other2, ...) + """ + # 核心:动态生成key函数 + if key is None: + key_func = lambda i: primary_list[i] # 默认:直接比较值 + else: + key_func = lambda i: key(primary_list[i]) # 自定义:对值应用key函数 + + # 获取排序索引 + indices = sorted(range(len(primary_list)), key=key_func, reverse=reverse) + + # 应用索引到所有列表(包括主列表) + all_lists = (primary_list,) + other_lists + result = tuple([lst[i] for i in indices] for lst in all_lists) + return result + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + frac_list = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000) + frac_list.append(frac) + #term_strs.append(f"{frac}·{symbol_str_final}") + term_strs.append(f"{frac}{symbol_str_final}") + + _, term_strs = sort_by_first_list(frac_list, term_strs, key=abs, reverse=True) + + return " + ".join(term_strs) + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def print_smoothness_indicators(expression,coeffs_list,k): + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + for r in range(k): + a_coeffs = coeffs_list[r] + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicatorOld(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + #print(f"k={k},a_coeffs={a_coeffs}") + coeffs_list.append( a_coeffs ) + + print_smoothness_indicators(total_result,coeffs_list,k) + +if __name__ == "__main__": + demo_smoothness_indicator(1) + demo_smoothness_indicator(2) + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/03e/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/03e/polynomial_operations.py new file mode 100644 index 00000000..a0d15dbb --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/03e/polynomial_operations.py @@ -0,0 +1,912 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + #print(f"sum_integrals_same_bounds polynomials={polynomials}") + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + #print(f" Integration Result for Term{idx+1}: {format_expression(integral_result)}") + print(f" Integration Result for Term{idx+1}: {format_expression_fraction(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def format_expression_fraction(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + if len(symbols) == 1: + term_strs.append(f"{frac_str}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{frac_str}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{frac_str}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\nInitial Polynomial Expressions (before integration):") + for i, poly in enumerate(squared_polynomials, 1): + print(f" Polynomial Term {i}: P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def to_fraction(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + return frac + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}·" + frac_star = f"{frac_str}" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f"k={k},polynomial_list={polynomial_list}") + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + lower_bound = -0.5 + upper_bound = 0.5 + domain = f"[{to_fraction(lower_bound)}, {to_fraction(upper_bound)}]" + print(f"\nStep-by-step Integration and Summation (integration domain: x∈{domain}):") + + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + #print(f"k={k},total_result={total_result}") + + # 打印最终结果 + print(f"\nFinal Aggregated Result (sum of all integrated terms):") + #formatted_result = format_expression(total_result) + formatted_result = format_expression_fraction(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def sort_by_first_list(primary_list, *other_lists, key=None, reverse=False): + """ + 根据第一个列表排序,同步调整任意数量其他列表 + + 参数: + primary_list: 主排序参考列表 + *other_lists: 其他需要同步排序的列表(可变参数) + key: 排序key函数(如abs, lambda x: x**2等) + reverse: 是否降序 + + 返回: + 元组: (sorted_primary, sorted_other1, sorted_other2, ...) + """ + # 核心:动态生成key函数 + if key is None: + key_func = lambda i: primary_list[i] # 默认:直接比较值 + else: + key_func = lambda i: key(primary_list[i]) # 自定义:对值应用key函数 + + # 获取排序索引 + indices = sorted(range(len(primary_list)), key=key_func, reverse=reverse) + + # 应用索引到所有列表(包括主列表) + all_lists = (primary_list,) + other_lists + result = tuple([lst[i] for i in indices] for lst in all_lists) + return result + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + frac_list = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000) + frac_list.append(frac) + #term_strs.append(f"{frac}·{symbol_str_final}") + term_strs.append(f"{frac}{symbol_str_final}") + + _, term_strs = sort_by_first_list(frac_list, term_strs, key=abs, reverse=True) + + return " + ".join(term_strs) + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def print_smoothness_indicators(expression,coeffs_list,k): + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + for r in range(k): + a_coeffs = coeffs_list[r] + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicatorOld(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + #print(f"k={k},a_coeffs={a_coeffs}") + coeffs_list.append( a_coeffs ) + + print_smoothness_indicators(total_result,coeffs_list,k) + +if __name__ == "__main__": + demo_smoothness_indicator(1) + demo_smoothness_indicator(2) + demo_smoothness_indicator(3) + demo_smoothness_indicator(4) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/smoothness_indicator/01/smoothness_indicator.py b/example/figure/1d/weno/interplate/smoothness_indicator/01/smoothness_indicator.py new file mode 100644 index 00000000..265150e9 --- /dev/null +++ b/example/figure/1d/weno/interplate/smoothness_indicator/01/smoothness_indicator.py @@ -0,0 +1,591 @@ +import numpy as np +import math +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_weno_substencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + rows, cols = sub_stencils.shape + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +def solve_weno_linear_weights(optimal_stencil: np.ndarray, sub_stencils: np.ndarray) -> np.ndarray: + """ + Solve for linear weights d such that: + optimal_stencil ≈ sum_j d[j] * sub_stencils[j] + + Prints the linear system and solved weights. + """ + + # Build target map + target_dict = build_target_offset_map(optimal_stencil) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(sub_stencils, target_dict) + return weights + +def demo_weno_linear_weights(weno_r: int): + """ + Demonstrate linear weight computation for WENO-r scheme. + + Parameters: + weno_r (int): Number of substencils (e.g., 3 for WENO5, 2 for WENO3) + """ + x_half = Fraction(1, 2) + global_stencil_width = 2 * weno_r - 1 # e.g., 5 for WENO3 + + # Left-biased (v_{i+1/2}^-) + substencils_L = generate_weno_substencils(stencil_width=weno_r, x_point=x_half) + optimal_L = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=x_half + ) + weights_L = solve_weno_linear_weights(optimal_L, substencils_L) + + # Right-biased (v_{i-1/2}^+) + substencils_R = generate_weno_substencils(stencil_width=weno_r, x_point=-x_half) + optimal_R = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=-x_half + ) + weights_R = solve_weno_linear_weights(optimal_R, substencils_R) + + return weights_L, weights_R + +def demo_weno_linear_weights_maxk(): + maxk = 3 + for k in range(1,maxk+1): + print(f"\n=== WENO{2*k-1} ===") + demo_weno_linear_weights(weno_r=k) + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def demo_smoothness_indicator(): + print(f'demo_smoothness_indicator') + + n = 5 + m = 2 + coeff, power = derivative_form(5, 2) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k-1 + cols = k-1 + matrix = np.empty((rows, cols), dtype=object) + #print(f'matrix=\n{matrix}') + + #x^1 x^2 x^3 + #d^1dx^1 1x^0 2x^1 3x^2 + #d^2dx^2 0x^0 2x^0 6x^1 + #d^3dx^3 0x^0 0x^0 6x^0 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j+1, i+1) + acoef = j + 1 + if coef == 0: + acoef = 0 + matrix[i][j] = (coef, acoef, power) + print(f"{coeff}x^{power}",end=' ') + print() + + print(f'matrix=\n{matrix}') + + +if __name__ == "__main__": + demo_smoothness_indicator() diff --git a/example/figure/1d/weno/interplate/smoothness_indicator/01a/smoothness_indicator.py b/example/figure/1d/weno/interplate/smoothness_indicator/01a/smoothness_indicator.py new file mode 100644 index 00000000..b25fb9c1 --- /dev/null +++ b/example/figure/1d/weno/interplate/smoothness_indicator/01a/smoothness_indicator.py @@ -0,0 +1,623 @@ +import numpy as np +import math +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_weno_substencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + rows, cols = sub_stencils.shape + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +def solve_weno_linear_weights(optimal_stencil: np.ndarray, sub_stencils: np.ndarray) -> np.ndarray: + """ + Solve for linear weights d such that: + optimal_stencil ≈ sum_j d[j] * sub_stencils[j] + + Prints the linear system and solved weights. + """ + + # Build target map + target_dict = build_target_offset_map(optimal_stencil) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(sub_stencils, target_dict) + return weights + +def demo_weno_linear_weights(weno_r: int): + """ + Demonstrate linear weight computation for WENO-r scheme. + + Parameters: + weno_r (int): Number of substencils (e.g., 3 for WENO5, 2 for WENO3) + """ + x_half = Fraction(1, 2) + global_stencil_width = 2 * weno_r - 1 # e.g., 5 for WENO3 + + # Left-biased (v_{i+1/2}^-) + substencils_L = generate_weno_substencils(stencil_width=weno_r, x_point=x_half) + optimal_L = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=x_half + ) + weights_L = solve_weno_linear_weights(optimal_L, substencils_L) + + # Right-biased (v_{i-1/2}^+) + substencils_R = generate_weno_substencils(stencil_width=weno_r, x_point=-x_half) + optimal_R = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=-x_half + ) + weights_R = solve_weno_linear_weights(optimal_R, substencils_R) + + return weights_L, weights_R + +def demo_weno_linear_weights_maxk(): + maxk = 3 + for k in range(1,maxk+1): + print(f"\n=== WENO{2*k-1} ===") + demo_weno_linear_weights(weno_r=k) + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + #print(f"{key}: {power_map[key]}") + mylist = power_map[key] + n = len( mylist ) + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + #print(f"idx={idx}",end =' ') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def demo_smoothness_indicator(): + print(f'demo_smoothness_indicator') + + n = 5 + m = 2 + coeff, power = derivative_form(5, 2) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k-1 + cols = k-1 + matrix = np.empty((rows, cols), dtype=object) + #print(f'matrix=\n{matrix}') + + #x^1 x^2 x^3 + #d^1dx^1 1x^0 2x^1 3x^2 + #d^2dx^2 0x^0 2x^0 6x^1 + #d^3dx^3 0x^0 0x^0 6x^0 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j+1, i+1) + acoef = j + 1 + matrix[i][j] = (coef, acoef, power) + print(f"{coeff}x^{power}",end=' ') + print() + + print(f'matrix=\n{matrix}') + power_map_list = [] + for i in range(rows): + power_map = defaultdict(list) + for j in range(cols): + coef, acoef, power = matrix[i][j] + if coef != 0: + power_map[power].append((coef, acoef)) + print(f"{coef}*a{acoef}*x^{power}",end=' ') + power_map_list.append(power_map) + print() + + print(f'power_map_list={power_map_list}') + for i in range(rows): + print_power_symbol(power_map_list[i]) + +if __name__ == "__main__": + demo_smoothness_indicator() diff --git a/example/figure/1d/weno/interplate/smoothness_indicator/01b/smoothness_indicator.py b/example/figure/1d/weno/interplate/smoothness_indicator/01b/smoothness_indicator.py new file mode 100644 index 00000000..3ec90af4 --- /dev/null +++ b/example/figure/1d/weno/interplate/smoothness_indicator/01b/smoothness_indicator.py @@ -0,0 +1,799 @@ +import numpy as np +import math +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_weno_substencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + rows, cols = sub_stencils.shape + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +def solve_weno_linear_weights(optimal_stencil: np.ndarray, sub_stencils: np.ndarray) -> np.ndarray: + """ + Solve for linear weights d such that: + optimal_stencil ≈ sum_j d[j] * sub_stencils[j] + + Prints the linear system and solved weights. + """ + + # Build target map + target_dict = build_target_offset_map(optimal_stencil) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(sub_stencils, target_dict) + return weights + +def demo_weno_linear_weights(weno_r: int): + """ + Demonstrate linear weight computation for WENO-r scheme. + + Parameters: + weno_r (int): Number of substencils (e.g., 3 for WENO5, 2 for WENO3) + """ + x_half = Fraction(1, 2) + global_stencil_width = 2 * weno_r - 1 # e.g., 5 for WENO3 + + # Left-biased (v_{i+1/2}^-) + substencils_L = generate_weno_substencils(stencil_width=weno_r, x_point=x_half) + optimal_L = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=x_half + ) + weights_L = solve_weno_linear_weights(optimal_L, substencils_L) + + # Right-biased (v_{i-1/2}^+) + substencils_R = generate_weno_substencils(stencil_width=weno_r, x_point=-x_half) + optimal_R = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=-x_half + ) + weights_R = solve_weno_linear_weights(optimal_R, substencils_R) + + return weights_L, weights_R + +def demo_weno_linear_weights_maxk(): + maxk = 3 + for k in range(1,maxk+1): + print(f"\n=== WENO{2*k-1} ===") + demo_weno_linear_weights(weno_r=k) + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def demo_smoothness_indicator(): + print(f'demo_smoothness_indicator') + + n = 5 + m = 2 + coeff, power = derivative_form(5, 2) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k-1 + cols = k-1 + matrix = np.empty((rows, cols), dtype=object) + #print(f'matrix=\n{matrix}') + + #x^1 x^2 x^3 + #d^1dx^1 1x^0 2x^1 3x^2 + #d^2dx^2 0x^0 2x^0 6x^1 + #d^3dx^3 0x^0 0x^0 6x^0 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j+1, i+1) + acoef = j + 1 + matrix[i][j] = (coef, acoef, power) + print(f"{coeff}x^{power}",end=' ') + print() + + print(f'matrix=\n{matrix}') + power_map_list = [] + for i in range(rows): + power_map = defaultdict(list) + for j in range(cols): + coef, acoef, power = matrix[i][j] + if coef != 0: + power_map[power].append((coef, acoef)) + print(f"{coef}*a{acoef}*x^{power}",end=' ') + power_map_list.append(power_map) + print() + + print(f'power_map_list={power_map_list}') + for i in range(rows): + print_power_symbol(power_map_list[i]) + +def square_polynomial(power_map): + """ + 多项式平方展开 + 参数: + power_map: {指数: [(数值系数, 符号索引), ...]} + 返回: + 展开后的结果字典 + """ + result = defaultdict(list) + sorted_exps = sorted(power_map.keys()) + + # 1. 计算每个项的平方 (a^2) + for exp in sorted_exps: + terms = power_map[exp] + new_exp = exp * 2 + + for coef, acoef in terms: + # 存储为 (数值系数, 符号索引, 是否平方标志) + result[new_exp].append((coef, acoef, True)) + + # 2. 计算交叉项 (2ab) + for i in range(len(sorted_exps)): + for j in range(i + 1, len(sorted_exps)): + exp_i, exp_j = sorted_exps[i], sorted_exps[j] + new_exp = exp_i + exp_j + + for coef_i, acoef_i in power_map[exp_i]: + for coef_j, acoef_j in power_map[exp_j]: + # 交叉项系数乘以2 + result[new_exp].append((2 * coef_i * coef_j, + (acoef_i, acoef_j), False)) + + return result + +def merge_and_simplify(power_map): + """合并同类项并化简符号乘积""" + simplified = defaultdict(dict) # exp: {符号键: 总系数} + + for exp, terms in power_map.items(): + for term_info in terms: + # 解析 term_info: (系数, 符号信息, 是否平方项) + if len(term_info) == 3: + coef, symbols_part, is_square = term_info + else: + # 兼容旧数据格式 + coef, symbols_part = term_info + is_square = False + + # 关键修复:根据标志计算有效系数 + if is_square: + # 平方项:系数需要平方 (c*a)^2 → c^2 * a^2 + effective_coef = coef * coef + else: + # 交叉项:系数已是最终值 2*c_i*c_j + effective_coef = coef + + # 生成标准化符号键 + if isinstance(symbols_part, tuple): + # 交叉项:(索引1, 索引2) + i, j = symbols_part + symbol_names = [f"a{i}", f"a{j}"] + else: + # 平方项:单个符号索引 + symbol_names = [f"a{symbols_part}", f"a{symbols_part}"] + + # 排序保证 a1*a2 和 a2*a1 被视为相同 + key = tuple(sorted(symbol_names)) + + # 累加系数 + simplified[exp][key] = simplified[exp].get(key, 0) + effective_coef + + # 转换回列表格式 + result = defaultdict(list) + for exp, term_dict in simplified.items(): + for symbol_key, total_coef in term_dict.items(): + result[exp].append((total_coef, symbol_key)) + + return result + +def format_term(coef, symbols): + """ + 格式化单项式,支持多种输入类型 + symbols 可能是: + - 整数 (如 1) → 表示 a1 + - 字符串 (如 'a1') → 直接使用 + - 元组 (如 ('a1', 'a2')) → a1*a2 或 a1^2 + """ + # 处理 symbols 为整数的情况(如 1 → 'a1') + if isinstance(symbols, int): + symbol_str = f"a{symbols}" + # 处理 symbols 为字符串的情况 + elif isinstance(symbols, str): + symbol_str = symbols + # 处理 symbols 为元组/列表的情况 + elif isinstance(symbols, (tuple, list)): + if len(symbols) == 1: + symbol_str = str(symbols[0]) + elif symbols[0] == symbols[1]: # 平方项 + symbol_str = f"{symbols[0]}^2" + else: # 不同符号相乘 + symbol_str = "*".join(str(s) for s in symbols) + else: + raise TypeError(f"Unsupported symbols type: {type(symbols)}") + + return f"{coef}*{symbol_str}" + +def print_expanded_polynomial(power_map): + """ + 打印展开后的多项式,不显示最后一行的"+" + """ + if not power_map: + print("0") + return + + sorted_keys = sorted(power_map.keys()) + + for idx, key in enumerate(sorted_keys): + terms = power_map[key] + + # 构建该项的内部表达式 + inner_terms = [] + for coef, symbols in terms: + inner_terms.append(format_term(coef, symbols)) + + # 打印 (内层表达式)*x^key + if len(inner_terms) == 1: + print(f"({inner_terms[0]})*x^{key}", end='') + else: + print(f"({' + '.join(inner_terms)})*x^{key}", end='') + + # 判断是否是最后一项 + if idx < len(sorted_keys) - 1: + print(" + ", end='') + else: + print() # 最后一项只换行 + +def square_polynomial_test(): + # ============= 测试代码 ============= + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, 1)], # x^0 项: 1*a1 + 1: [(2, 2)] # x^1 项: 2*a2 + } + + print("原始多项式1:") + print_expanded_polynomial(poly1) + # 输出: (1*a1)*x^0 + (2*a2)*x^1 + + print("\n平方展开后:") + expanded1 = square_polynomial(poly1) + print(f"expanded1={expanded1}") + merged1 = merge_and_simplify(expanded1) + print(f"merged1={merged1}") + print_expanded_polynomial(merged1) + # 期望: (1*a1^2)*x^0 + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, 2)] # x^0 项: 2*a2 + } + + print("\n原始多项式2:") + print_expanded_polynomial(poly2) + # 输出: (2*a2)*x^0 + + print("\n平方展开后:") + expanded2 = square_polynomial(poly2) + merged2 = merge_and_simplify(expanded2) + print_expanded_polynomial(merged2) + # 期望: (4*a2^2)*x^0 + +if __name__ == "__main__": + #demo_smoothness_indicator() + square_polynomial_test() + + + diff --git a/example/figure/1d/weno/interplate/weno5_smoothness/01/weno5_smoothness_sympy.py b/example/figure/1d/weno/interplate/weno5_smoothness/01/weno5_smoothness_sympy.py new file mode 100644 index 00000000..f043db33 --- /dev/null +++ b/example/figure/1d/weno/interplate/weno5_smoothness/01/weno5_smoothness_sympy.py @@ -0,0 +1,56 @@ +import sympy as sp + +def compute_weno5_beta_k(points_values, k): + """ + 计算 WENO5 子模板 k 的光滑因子 β_k,使用 SymPy 符号积分。 + + 参数: + - points_values: list of sympy symbols or floats, e.g., [v_im2, v_im1, vi] for k=0 + - k: int, 子模板索引 (0: 左偏, 1: 中心, 2: 右偏) + + 返回: + - sympy Expr: 符号表达式 of β_k + """ + if k not in [0, 1, 2]: + raise ValueError("k must be 0, 1, or 2 for WENO5") + + # 根据 k 定义点坐标 (Δx=1, 参考 x_i=0) + if k == 0: + coords = [-2, -1, 0] + elif k == 1: + coords = [-1, 0, 1] + else: # k=2 + coords = [0, 1, 2] + + x = sp.symbols('x') + v0, v1, v2 = points_values # 符号或数值 + + # 拉格朗日基函数 + l0 = ((x - coords[1]) * (x - coords[2])) / ((coords[0] - coords[1]) * (coords[0] - coords[2])) * v0 + l1 = ((x - coords[0]) * (x - coords[2])) / ((coords[1] - coords[0]) * (coords[1] - coords[2])) * v1 + l2 = ((x - coords[0]) * (x - coords[1])) / ((coords[2] - coords[0]) * (coords[2] - coords[1])) * v2 + + # 多项式 p_k(x) + p_k = l0 + l1 + l2 + + # 导数 (m=1 to 3) + dp1 = sp.diff(p_k, x) + dp2 = sp.diff(p_k, x, 2) + dp3 = sp.diff(p_k, x, 3) # 0 for quadratic + + # 积分限 + a, b = -sp.Rational(1, 2), sp.Rational(1, 2) + + # 各 m 项 + int_m1 = sp.integrate(dp1**2, (x, a, b)) + int_m2 = sp.integrate(dp2**2, (x, a, b)) + int_m3 = sp.integrate(dp3**2, (x, a, b)) + + # β_k + beta_k = int_m1 + int_m2 + int_m3 + return sp.simplify(beta_k) + +# 示例使用 (符号) +v_im2, v_im1, vi = sp.symbols('v_{i-2} v_{i-1} v_i') +beta0 = compute_weno5_beta_k([v_im2, v_im1, vi], 0) +print(beta0) # 输出: (4/3)*v_{i-2}^2 + (25/3)*v_{i-1}^2 + (10/3)*v_i^2 - (19/3)*v_{i-2}*v_{i-1} + ... \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/01/xi.py b/example/figure/1d/weno/interplate/xi/01/xi.py new file mode 100644 index 00000000..697d9d2d --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/01/xi.py @@ -0,0 +1,12 @@ +from fractions import Fraction + +i = 5 +id = 1 +j = i + id +#half = Fraction("1/2") +half = Fraction(1,2) +xia = id - half +xib = id + half +print(f'half={half}') +print(f'xia,xib={xia},{xib}') + diff --git a/example/figure/1d/weno/interplate/xi/01a/xi.py b/example/figure/1d/weno/interplate/xi/01a/xi.py new file mode 100644 index 00000000..1712ad77 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/01a/xi.py @@ -0,0 +1,16 @@ +from fractions import Fraction + +def calxi(j): + half = Fraction(1,2) + xia = j - half + xib = j + half + return xia, xib + +jst = -2 +jed = 2 + +for j in range(jst, jed+1): + xia, xib = calxi(j) + #print(f'j={j:>2} xia,b=[{xia},{xib}]') + print(f'j={j:>2} xia,b=[{str(xia):>4},{str(xib):>4}]') + diff --git a/example/figure/1d/weno/interplate/xi/02/xi.py b/example/figure/1d/weno/interplate/xi/02/xi.py new file mode 100644 index 00000000..a706a3bd --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02/xi.py @@ -0,0 +1,12 @@ +from fractions import Fraction + +def calxi(x,j): + return (x**(j+1))/(j+1) + +jst = 0 +jed = 4 +x = Fraction(1,2) +for j in range(0, jed+1): + v = calxi(x,j) + print(f'Intergral[({x})^{j}]={v}') + diff --git a/example/figure/1d/weno/interplate/xi/02a/xi.py b/example/figure/1d/weno/interplate/xi/02a/xi.py new file mode 100644 index 00000000..a706a3bd --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02a/xi.py @@ -0,0 +1,12 @@ +from fractions import Fraction + +def calxi(x,j): + return (x**(j+1))/(j+1) + +jst = 0 +jed = 4 +x = Fraction(1,2) +for j in range(0, jed+1): + v = calxi(x,j) + print(f'Intergral[({x})^{j}]={v}') + diff --git a/example/figure/1d/weno/interplate/xi/02b/xi.py b/example/figure/1d/weno/interplate/xi/02b/xi.py new file mode 100644 index 00000000..a070f96e --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02b/xi.py @@ -0,0 +1,25 @@ +from fractions import Fraction + +def intregral_xi(x,j): + return (x**(j+1))/(j+1) + +def calxi(j): + half = Fraction(1,2) + xia = j - half + xib = j + half + return xia, xib + +jst = -2 +jed = 2 + +for j in range(jst, jed+1): + xia, xib = calxi(j) + print(f'j={j:>2} xia,b=[{str(xia):>4},{str(xib):>4}]') + ist = 0 + ied = 4 + for i in range(ist, ied+1): + v1 = intregral_xi(xia,i) + v2 = intregral_xi(xib,i) + print(f' v1={v1} v2={v2} diff ={v2-v1}') + + diff --git a/example/figure/1d/weno/interplate/xi/02c/xi.py b/example/figure/1d/weno/interplate/xi/02c/xi.py new file mode 100644 index 00000000..547b34cd --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02c/xi.py @@ -0,0 +1,26 @@ +from fractions import Fraction + +def intregral_xi(x,j): + return (x**(j+1))/(j+1) + +def calxi(j): + half = Fraction(1,2) + xia = j - half + xib = j + half + return xia, xib + +jst = -2 +jed = 2 + +for j in range(jst, jed+1): + xia, xib = calxi(j) + print(f'j={j:>2},interval=[{str(xia):>4},{str(xib):>4}]',end=' ') + ist = 0 + ied = 4 + for i in range(ist, ied+1): + v1 = intregral_xi(xia,i) + v2 = intregral_xi(xib,i) + print(f'{v2-v1}',end=' ') + print() + + diff --git a/example/figure/1d/weno/interplate/xi/02d/xi.py b/example/figure/1d/weno/interplate/xi/02d/xi.py new file mode 100644 index 00000000..ae550bd1 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02d/xi.py @@ -0,0 +1,45 @@ +from fractions import Fraction + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def calxi(j): + half = Fraction(1, 2) + xia = j - half + xib = j + half + return xia, xib + +jst = -2 +jed = 2 + +# 先计算所有值,找出每列需要的最大宽度(可选,更极致对齐) +# 这里直接用固定宽度,也已经非常整齐 + +print(f"{'j':>3} {'interval':>14} i=0 i=1 i=2 i=3 i=4") +print("-" * 68) + +for j in range(jst, jed + 1): + xia, xib = calxi(j) + # 把区间格式化为固定宽度字符串 + interval_str = f"[{xia:>4},{xib:>4}]" + + line = f"{j:>2} {interval_str} " + + for i in range(0, 5): # i 从 0 到 4 + v1 = integral_xi(xia, i) + v2 = integral_xi(xib, i) + diff = v2 - v1 + + if diff == 0: + s = "0" + elif diff == 1: + s = "1" + elif diff == -1: + s = "-1" + else: + s = str(diff) + + # 每列固定 11 个字符宽度,居中对齐(足够容纳 1441/80 这类最长的) + line += f"{s:^11}" + + print(line) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/02e/xi.py b/example/figure/1d/weno/interplate/xi/02e/xi.py new file mode 100644 index 00000000..a7e4c4a4 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02e/xi.py @@ -0,0 +1,17 @@ +from fractions import Fraction + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +print(f"{'j':>3} {'interval':>14} " + " ".join(f"i={i:^8}" for i in range(5))) +print("-" * 72) + +for j in range(-2, 3): + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + print(f"{j:>3} [{xia:>4},{xib:>4}] ", end="") + for i in range(5): + val = integral_xi(xib, i) - integral_xi(xia, i) + s = "0" if val == 0 else str(val) + print(f"{s:^11}", end="") + print() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/02f/xi.py b/example/figure/1d/weno/interplate/xi/02f/xi.py new file mode 100644 index 00000000..2d47938b --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02f/xi.py @@ -0,0 +1,24 @@ +from fractions import Fraction + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +data = [] +for j in range(-2, 3): + xia = Fraction(j) - Fraction(1, 2) + xib = Fraction(j) + Fraction(1, 2) + row = [j, f"[{xia}, {xib}]"] + for i in range(5): + val = integral_xi(xib, i) - integral_xi(xia, i) + row.append(0 if val == 0 else val) # 0 显示为整数 0,其他保持 Fraction + data.append(row) + +import pandas as pd +df = pd.DataFrame(data, columns=['j', 'interval'] + [f'i={i}' for i in range(5)]) + +# 关键:设置 pandas 显示选项 + 直接 print +pd.set_option('display.max_columns', None) +pd.set_option('display.width', None) +pd.set_option('display.colheader_justify', 'center') + +print(df.to_string(index=False)) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/02g/xi.py b/example/figure/1d/weno/interplate/xi/02g/xi.py new file mode 100644 index 00000000..6a7cc9d5 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02g/xi.py @@ -0,0 +1,76 @@ +from fractions import Fraction + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def calxi(j): + half = Fraction(1, 2) + return j - half, j + half + +jst, jed = -2, 2 + +# Step 1: 生成所有行数据(先单独处理 interval 的三部分:左括号、分数值、右括号) +rows = [] +left_brackets = [] # 全部是 "[" +right_brackets = [] # 全部是 "]" +intervals_a = [] # xia +intervals_b = [] # xib + +for j in range(jst, jed + 1): + xia, xib = calxi(j) + row = [str(j)] + for i in range(5): + diff = integral_xi(xib, i) - integral_xi(xia, i) + s = "0" if diff == 0 else ("1" if diff == 1 else ("-1" if diff == -1 else str(diff))) + row.append(s) + rows.append(row) + + left_brackets.append("[") + intervals_a.append(str(xia)) + intervals_b.append(str(xib)) + right_brackets.append("]") + +# Step 2: 标题(j 单独一列,interval 拆成 [ | 值 | ] 三列) +headers = ["j", "", "interval", "", "", "i=0", "i=1", "i=2", "i=3", "i=4"] +# 对应列: 0:j 1:[ 2:xia 3:xib 4:] 5~9:i=0~i=4 + +# Step 3: 计算每一列最大宽度 +col_widths = [0] * len(headers) + +# j 列 + 数值列 +for i, h in enumerate(headers): + col_widths[i] = max(col_widths[i], len(h)) + +for row, a, b in zip(rows, intervals_a, intervals_b): + col_widths[0] = max(col_widths[0], len(row[0])) # j + col_widths[2] = max(col_widths[2], len(a)) # xia + col_widths[3] = max(col_widths[3], len(b)) # xib + for k in range(5): # i=0~4 + col_widths[5 + k] = max(col_widths[5 + k], len(row[1 + k])) + +# 括号列固定宽度 1 就够了,但我们也算进去 +col_widths[1] = max(col_widths[1], len("[")) # 1 +col_widths[4] = max(col_widths[4], len("]")) # 1 + +# 为了美观,给每列再 +1 空格间隔(除了最右侧可以不加) +widths_with_space = [w + 1 for w in col_widths] + +# Step 4: 打印函数 +def print_line(parts, aligns=None): + if aligns is None: + aligns = ['^'] * len(parts) # 默认居中 + line = "" + for p, w, align in zip(parts, widths_with_space, aligns): + line += f"{p:{align}{w}}" + print(line.rstrip()) # 去掉行末多余空格 + +# 表头(interval 跨三列,我们手动合并显示) +print_line(headers[:5] + headers[5:], aligns=['^','^','^','^','^','^','^','^','^','^']) +print_line(["", "interval","","","","i=0","i=1","i=2","i=3","i=4"], + aligns=['<','^','^','^','>','^','^','^','^','^']) # interval 标题居中跨三列 +print("-" * (sum(widths_with_space) - 1)) + +# 数据行 +for j_row, lb, a, b, rb, data_row in zip(rows, left_brackets, intervals_a, intervals_b, right_brackets, rows): + parts = [data_row[0], lb, a, b, rb] + data_row[1:] + print_line(parts) diff --git a/example/figure/1d/weno/interplate/xi/03/xi.py b/example/figure/1d/weno/interplate/xi/03/xi.py new file mode 100644 index 00000000..7385a902 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/03/xi.py @@ -0,0 +1,18 @@ +from fractions import Fraction + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +print(f"{'j':>3} {'interval':>14} " + " ".join(f"i={i:^8}" for i in range(5))) +print("-" * 72) + +vv = [-1,0,1] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + print(f"{j:>3} [{xia:>4},{xib:>4}] ", end="") + for i in range(5): + val = integral_xi(xib, i) - integral_xi(xia, i) + s = "0" if val == 0 else str(val) + print(f"{s:^11}", end="") + print() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/03a/xi.py b/example/figure/1d/weno/interplate/xi/03a/xi.py new file mode 100644 index 00000000..1196f356 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/03a/xi.py @@ -0,0 +1,28 @@ +import numpy as np +from fractions import Fraction + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +vv = [-1,0,1] +isize = len(vv) +print(f"{'j':>3} {'interval':>14} " + " ".join(f"i={i:^8}" for i in range(isize))) +print("-" * 72) + +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + print(f"{j:>3} [{xia:>4},{xib:>4}] ", end="") + a_list = [] + for i in range( isize ): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + s = "0" if val == 0 else str(val) + print(f"{s:^11}", end="") + print() + arrays_list.append(a_list) + +# 使用 vstack 函数将列表中的数组堆叠成一个矩阵 +matrix = np.vstack(arrays_list) +print(f'matrix={matrix}') \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/03b/xi.py b/example/figure/1d/weno/interplate/xi/03b/xi.py new file mode 100644 index 00000000..bd34b1ae --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/03b/xi.py @@ -0,0 +1,88 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix): + # 将矩阵转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in matrix]) + + # 转换为字符串矩阵并计算每列的最大宽度 + str_matrix = [] + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + col_widths = [0] * cols # 每列的最大宽度 + + # 将数字转换为字符串,并记录每列最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 打印矩阵,每列等宽右对齐,添加逗号 + #print("Matrix in Fraction Form:") + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + # 右对齐,使用该列的最大宽度 + formatted_element = f"{element:>{col_widths[j]}}" + # 除最后一列外添加逗号和空格 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + # 拼接一行并打印 + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + + +vv = [-1,0,1] +isize = len(vv) +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + #print(f"{j:>3} [{xia:>4},{xib:>4}] ", end="") + a_list = [] + for i in range( isize ): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + s = "0" if val == 0 else str(val) + #print(f"{s:^11}", end="") + #print() + arrays_list.append(a_list) + +# 使用 vstack 函数将列表中的数组堆叠成一个矩阵 +matrix = np.vstack(arrays_list) +print_matrix_fraction(matrix) + +# 计算逆矩阵 +inverse = inverse_matrix(matrix) + +print("\nInverse Matrix in Fraction Form:") +print_matrix_fraction(inverse) + +# 计算两个矩阵的乘积 +product = np.dot(matrix, inverse) + +print("\nProduct of Matrix and Inverse Matrix:") +print_matrix_fraction(product) diff --git a/example/figure/1d/weno/interplate/xi/03c/xi.py b/example/figure/1d/weno/interplate/xi/03c/xi.py new file mode 100644 index 00000000..7789fb39 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/03c/xi.py @@ -0,0 +1,128 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix): + # 将矩阵转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in matrix]) + + # 转换为字符串矩阵并计算每列的最大宽度 + str_matrix = [] + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + col_widths = [0] * cols # 每列的最大宽度 + + # 将数字转换为字符串,并记录每列最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 打印矩阵,每列等宽右对齐,添加逗号 + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + # 右对齐,使用该列的最大宽度 + formatted_element = f"{element:>{col_widths[j]}}" + # 除最后一列外添加逗号和空格 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + # 拼接一行并打印 + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A"): + """ + 输出矩阵的 LaTeX 格式 + :param matrix: 输入矩阵(列表嵌套或numpy数组) + :param matrix_name: 矩阵名称(用于注释) + """ + # 转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + # 整数直接输出分子 + row_elements.append(f"{f.numerator}") + else: + # 分数输出为 \frac{分子}{分母} + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 每行元素用 & 分隔,行尾加 \\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 打印LaTeX格式(用 === 分隔,方便复制) + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +vv = [-1,0,1] +isize = len(vv) +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(isize): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + +# 使用 vstack 函数将列表中的数组堆叠成一个矩阵 +matrix = np.vstack(arrays_list) + +# 打印原始矩阵(分数字符串格式 + LaTeX格式) +print("Original Matrix in Fraction Form:") +print_matrix_fraction(matrix) +print_matrix_latex(matrix, "Original Matrix") + +# 计算逆矩阵 +inverse = inverse_matrix(matrix) + +# 打印逆矩阵(分数字符串格式 + LaTeX格式) +print("Inverse Matrix in Fraction Form:") +print_matrix_fraction(inverse) +print_matrix_latex(inverse, "Inverse Matrix") + +# 计算两个矩阵的乘积 +product = np.dot(matrix, inverse) + +# 打印乘积矩阵(分数字符串格式 + LaTeX格式) +print("Product of Matrix and Inverse Matrix:") +print_matrix_fraction(product) +print_matrix_latex(product, "Matrix Product (Identity Matrix)") \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/03d/xi.py b/example/figure/1d/weno/interplate/xi/03d/xi.py new file mode 100644 index 00000000..0156753a --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/03d/xi.py @@ -0,0 +1,169 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +vv = [-1,0,1] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xx = compute_coef(Fraction(1,2), k) +print(f"xx(一维行向量):") +print_matrix_fraction(xx) # 默认行向量格式 +print_matrix_latex(xx, "Vector xx (Row Vector)") # LaTeX行向量 + +# 可选:按列向量格式打印xx +print(f"\nxx(一维列向量):") +print_matrix_fraction(xx, is_column_vector=True) +print_matrix_latex(xx, "Vector xx (Column Vector)", is_column_vector=True) + +# 构建二维矩阵并打印 +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(k): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + +matrix = np.vstack(arrays_list) +print("\nOriginal Matrix in Fraction Form:") +print_matrix_fraction(matrix) +print_matrix_latex(matrix, "Original Matrix") + +# 计算并打印逆矩阵 +inverse = inverse_matrix(matrix) +print(f"inverse(二维矩阵):") +print_matrix_fraction(inverse) +print_matrix_latex(inverse, "Inverse Matrix") + +# 计算并打印乘积矩阵 +product = np.dot(matrix, inverse) +print("Product of Matrix and Inverse Matrix:") +print_matrix_fraction(product) +print_matrix_latex(product, "Matrix Product (Identity Matrix)") \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/03e/xi.py b/example/figure/1d/weno/interplate/xi/03e/xi.py new file mode 100644 index 00000000..f729d67f --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/03e/xi.py @@ -0,0 +1,173 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +vv = [-1,0,1] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xx = compute_coef(Fraction(1,2), k) +print(f"xx(一维行向量):") +print_matrix_fraction(xx) # 默认行向量格式 +print_matrix_latex(xx, "Vector xx (Row Vector)") # LaTeX行向量 + +# 可选:按列向量格式打印xx +print(f"\nxx(一维列向量):") +print_matrix_fraction(xx, is_column_vector=True) +print_matrix_latex(xx, "Vector xx (Column Vector)", is_column_vector=True) + +# 构建二维矩阵并打印 +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(k): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + +matrix = np.vstack(arrays_list) +print("\nOriginal Matrix in Fraction Form:") +print_matrix_fraction(matrix) +print_matrix_latex(matrix, "Original Matrix") + +# 计算并打印逆矩阵 +inverse = inverse_matrix(matrix) +print(f"inverse(二维矩阵):") +print_matrix_fraction(inverse) +print_matrix_latex(inverse, "Inverse Matrix") + +# 计算并打印乘积矩阵 +product = np.dot(matrix, inverse) +print("Product of Matrix and Inverse Matrix:") +print_matrix_fraction(product) +print_matrix_latex(product, "Matrix Product (Identity Matrix)") + +yy = np.dot(xx, inverse) +print(f"yy:") +print_matrix_fraction(yy) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/04/xi.py b/example/figure/1d/weno/interplate/xi/04/xi.py new file mode 100644 index 00000000..d50ab397 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/04/xi.py @@ -0,0 +1,212 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = f'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0: + if var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + else: + fv1 = sf + ' ' + str(ff) + + var = f'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +# 构建二维矩阵并打印 +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(k): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + +matrix = np.vstack(arrays_list) +print("\nOriginal Matrix in Fraction Form:") +print_matrix_fraction(matrix) +print_matrix_latex(matrix, "Original Matrix") + +# 计算并打印逆矩阵 +inverse = inverse_matrix(matrix) +print(f"inverse(二维矩阵):") +print_matrix_fraction(inverse) +print_matrix_latex(inverse, "Inverse Matrix") + +# 计算并打印乘积矩阵 +product = np.dot(matrix, inverse) +print("Product of Matrix and Inverse Matrix:") +print_matrix_fraction(product) +print_matrix_latex(product, "Matrix Product (Identity Matrix)") + +xpcoef = np.dot(xp, inverse) +xncoef = np.dot(xn, inverse) +print(f"xpcoef:") +print_matrix_fraction(xpcoef) + +print_formula(xpcoef,vv) + +print(f"xncoef:") +print_matrix_fraction(xncoef) +print_formula(xncoef,vv) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/04a/xi.py b/example/figure/1d/weno/interplate/xi/04a/xi.py new file mode 100644 index 00000000..9686e05c --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/04a/xi.py @@ -0,0 +1,209 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = f'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = f'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +# 构建二维矩阵并打印 +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(k): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + +matrix = np.vstack(arrays_list) +print("\nOriginal Matrix in Fraction Form:") +print_matrix_fraction(matrix) +print_matrix_latex(matrix, "Original Matrix") + +# 计算并打印逆矩阵 +inverse = inverse_matrix(matrix) +print(f"inverse(二维矩阵):") +print_matrix_fraction(inverse) +print_matrix_latex(inverse, "Inverse Matrix") + +# 计算并打印乘积矩阵 +product = np.dot(matrix, inverse) +print("Product of Matrix and Inverse Matrix:") +print_matrix_fraction(product) +print_matrix_latex(product, "Matrix Product (Identity Matrix)") + +xpcoef = np.dot(xp, inverse) +xncoef = np.dot(xn, inverse) +print(f"xpcoef:") +print_matrix_fraction(xpcoef) + +print_formula(xpcoef,vv) + +print(f"xncoef:") +print_matrix_fraction(xncoef) +print_formula(xncoef,vv) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/05/xi.py b/example/figure/1d/weno/interplate/xi/05/xi.py new file mode 100644 index 00000000..1ed9d853 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05/xi.py @@ -0,0 +1,192 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a = [Fraction(s) for s in a_str] +print_matrix_fraction(a) +ii = 2 +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) diff --git a/example/figure/1d/weno/interplate/xi/05a/xi.py b/example/figure/1d/weno/interplate/xi/05a/xi.py new file mode 100644 index 00000000..e0dd67a4 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05a/xi.py @@ -0,0 +1,205 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +N = len(a) +M = len(a0) +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + diff --git a/example/figure/1d/weno/interplate/xi/05b/xi.py b/example/figure/1d/weno/interplate/xi/05b/xi.py new file mode 100644 index 00000000..6627f525 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05b/xi.py @@ -0,0 +1,229 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +N = len(a) +M = len(a0) +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/05c/xi.py b/example/figure/1d/weno/interplate/xi/05c/xi.py new file mode 100644 index 00000000..ef102a67 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05c/xi.py @@ -0,0 +1,246 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +N = len(a) +M = len(a0) +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) + +rows=M +cols=M +arr = np.empty((rows, cols), dtype=object) +print(arr) +for i in range(rows): + for j in range(cols): + arr[i, j] = Fraction(i+1, j+1) +print(arr) +print_matrix_fraction(arr) + +for i in range(rows): + for j in range(cols): + arr[i, j] = alist[i][j] + +print_matrix_fraction(arr) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/05d/xi.py b/example/figure/1d/weno/interplate/xi/05d/xi.py new file mode 100644 index 00000000..46371b34 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05d/xi.py @@ -0,0 +1,269 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +N = len(a) +M = len(a0) +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) + +rows=M +cols=M +arr = np.empty((rows, cols), dtype=object) + +for i in range(rows): + for j in range(cols): + arr[i, j] = alist[i][j] + +print_matrix_fraction(arr) +print_matrix_fraction(alist) + +for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + mystr += coef_tostring(alist[i][j],j)+ f"*v[i{var_rj}]" + print(f'mystr={mystr}') + + \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/05e/xi.py b/example/figure/1d/weno/interplate/xi/05e/xi.py new file mode 100644 index 00000000..1e9bacfb --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05e/xi.py @@ -0,0 +1,281 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +N = len(a) +M = len(a0) +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) + +rows=M +cols=M +print_matrix_fraction(alist) + +widths = np.empty(M, dtype=int) + +for j in range(rows): + w = 0 + for i in range(cols): + ww = len( coef_tostring(alist[i][j],j) ) + w = max(w, ww) + widths[j] = w + +print(f'widths={widths}') + +for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + mystr += coef_tostring(alist[i][j],j)+ f"*v[i{var_rj}]" + print(f'mystr={mystr}') + +for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + ss = f"{coef_tostring(alist[i][j],j):>{widths[j]}}" + mystr += ss+ f"*v[i{var_rj}]" + print(f'mystr={mystr}') diff --git a/example/figure/1d/weno/interplate/xi/05f/xi.py b/example/figure/1d/weno/interplate/xi/05f/xi.py new file mode 100644 index 00000000..75a32061 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05f/xi.py @@ -0,0 +1,284 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +N = len(a) +M = len(a0) +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) + +rows=M +cols=M +print_matrix_fraction(alist) + +widths = np.empty(M, dtype=int) + +for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + +print(f'widths={widths}') + +for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + #print(f'ss,absv={ss}{absv}') + mystr += ss + tt + f"*v[i{var_rj}]" + print(f'mystr={mystr}') diff --git a/example/figure/1d/weno/interplate/xi/05g/xi.py b/example/figure/1d/weno/interplate/xi/05g/xi.py new file mode 100644 index 00000000..301595c3 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05g/xi.py @@ -0,0 +1,310 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist): + rows, cols = alist.shape + print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + #print(f'ss,absv={ss}{absv}') + mystr += ss + tt + f"*v[i{var_rj}]" + print(f'vi+1/2,r={r}={mystr}') + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +coef_matrix = np.array(alist) + +N = len(a) +M = len(a0) + +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) + +rows=M +cols=M +print_matrix_fraction(alist) + +print_coef_formula(coef_matrix) + +kk=3 +for r in range(kk): + #-r+l + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + print("\nOriginal Matrix in Fraction Form:") + print_matrix_fraction(matrix) diff --git a/example/figure/1d/weno/interplate/xi/06/xi.py b/example/figure/1d/weno/interplate/xi/06/xi.py new file mode 100644 index 00000000..740d7652 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/06/xi.py @@ -0,0 +1,321 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist): + rows, cols = alist.shape + print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + #print(f'ss,absv={ss}{absv}') + mystr += ss + tt + f"*v[i{var_rj}]" + print(f'vi+1/2,r={r}={mystr}') + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +coef_matrix = np.array(alist) + +N = len(a) +M = len(a0) + +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) + +rows=M +cols=M +print_matrix_fraction(alist) + +print_coef_formula(coef_matrix) + +kk=3 +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), kk) +xn = compute_coef(-Fraction(1,2), kk) + +rows_list = [] +for r in range(kk): + #-r+l + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + #print(f'r={r}') + #print("\nOriginal Matrix in Fraction Form:") + #print_matrix_fraction(matrix) + inverse = inverse_matrix(matrix) + yy = np.dot(xp, inverse) + #print(f"yy:") + #print_matrix_fraction(yy) + rows_list.append(yy) + +mymat = np.vstack(rows_list) + +print_matrix_fraction(mymat) diff --git a/example/figure/1d/weno/interplate/xi/06a/xi.py b/example/figure/1d/weno/interplate/xi/06a/xi.py new file mode 100644 index 00000000..c2848b4d --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/06a/xi.py @@ -0,0 +1,249 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist): + rows, cols = alist.shape + print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + #print(f'ss,absv={ss}{absv}') + mystr += ss + tt + f"*v[i{var_rj}]" + print(f'vi+1/2,r={r}={mystr}') + +kk=3 +xp = compute_coef(Fraction(1,2), kk) +xn = compute_coef(-Fraction(1,2), kk) + +rows_list = [] +for r in range(kk): + #-r+l + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + #print(f'r={r}') + #print("\nOriginal Matrix in Fraction Form:") + #print_matrix_fraction(matrix) + inverse = inverse_matrix(matrix) + yy = np.dot(xp, inverse) + #print(f"yy:") + #print_matrix_fraction(yy) + rows_list.append(yy) + +mymat = np.vstack(rows_list) + +print_matrix_fraction(mymat) +print_coef_formula(mymat) diff --git a/example/figure/1d/weno/interplate/xi/06b/xi.py b/example/figure/1d/weno/interplate/xi/06b/xi.py new file mode 100644 index 00000000..38a45923 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/06b/xi.py @@ -0,0 +1,223 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist,xfrac): + rows, cols = alist.shape + print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + #print(f'ss,absv={ss}{absv}') + mystr += ss + tt + f"*v[i{var_rj}]" + sxf = '' + if xfrac >= 0: + sxf='+' + print(f'vi{sxf}{xfrac},r={r}={mystr}') + +def calc_coef_formula(kk, xfrac): + xm = compute_coef(xfrac, kk) + rows_list = [] + for r in range(kk): + #-r+l + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + inverse = inverse_matrix(matrix) + ym = np.dot(xm, inverse) + rows_list.append(ym) + + return np.vstack(rows_list) + + +kk=3 +xfrac = Fraction(1,2) +xp = compute_coef(Fraction(1,2), kk) +xn = compute_coef(-Fraction(1,2), kk) + +mymat1 = calc_coef_formula(kk, xfrac) +mymat2 = calc_coef_formula(kk, -xfrac) + +print_matrix_fraction(mymat1) +print_coef_formula(mymat1,xfrac) + +print_matrix_fraction(mymat2) +print_coef_formula(mymat2,-xfrac) + diff --git a/example/figure/1d/weno/interplate/xi/06c/xi.py b/example/figure/1d/weno/interplate/xi/06c/xi.py new file mode 100644 index 00000000..fc444687 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/06c/xi.py @@ -0,0 +1,178 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist,xfrac,ishift=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = ishift - r + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + #print(f'vi{sxf}{xfrac_new}({slr}),r={r}={mystr}') + print(f'vi{sxf}{xfrac_new}({slr}),{r}={mystr}') + print() + +def calc_coef_formula(kk, xfrac): + xm = compute_coef(xfrac, kk) + rows_list = [] + for r in range(kk): + #-r+l + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + inverse = inverse_matrix(matrix) + ym = np.dot(xm, inverse) + rows_list.append(ym) + + return np.vstack(rows_list) + + +kk=3 +xfrac = Fraction(1,2) +xp = compute_coef(Fraction(1,2), kk) +xn = compute_coef(-Fraction(1,2), kk) + +mymat1 = calc_coef_formula(kk, xfrac) +mymat2 = calc_coef_formula(kk, -xfrac) + +print_matrix_fraction(mymat1) +print_coef_formula(mymat1,xfrac) + +print_matrix_fraction(mymat2) +print_coef_formula(mymat2,-xfrac,1) + diff --git a/example/figure/1d/weno/interplate/xi/07/xi.py b/example/figure/1d/weno/interplate/xi/07/xi.py new file mode 100644 index 00000000..c22ebb42 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07/xi.py @@ -0,0 +1,195 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist,xfrac,ishift=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = ishift - r + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + #print(f'vi{sxf}{xfrac_new}({slr}),r={r}={mystr}') + print(f'vi{sxf}{xfrac_new}({slr}),{r}={mystr}') + print() + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + #xm = compute_coef(xfrac, kk) + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +kk=3 +xfrac = Fraction(1,2) +xp = compute_coef(Fraction(1,2), kk) +xn = compute_coef(-Fraction(1,2), kk) + +mymat1 = calc_coef_formula(kk, xfrac) +mymat2 = calc_coef_formula(kk, -xfrac) + +print_matrix_fraction(mymat1) +print_coef_formula(mymat1,xfrac) + +print_matrix_fraction(mymat2) +#print_coef_formula(mymat2,-xfrac,1) +print_coef_formula(mymat2,-xfrac) + diff --git a/example/figure/1d/weno/interplate/xi/07a/xi.py b/example/figure/1d/weno/interplate/xi/07a/xi.py new file mode 100644 index 00000000..e50394c3 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07a/xi.py @@ -0,0 +1,200 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist,xfrac,ishift=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = ishift - r + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + #print(f'vi{sxf}{xfrac_new}({slr}),r={r}={mystr}') + print(f'vi{sxf}{xfrac_new}({slr}),{r}={mystr}') + print() + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +#print_coef_formula(mymat3R,-xfrac,1) +print_coef_formula(mymat3R,-xfrac) + diff --git a/example/figure/1d/weno/interplate/xi/07b/xi.py b/example/figure/1d/weno/interplate/xi/07b/xi.py new file mode 100644 index 00000000..628a5df5 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07b/xi.py @@ -0,0 +1,211 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + if rows == 1: + r = rin + for j in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = ishift - r + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + #print(f'vi{sxf}{xfrac_new}({slr}),r={r}={mystr}') + print(f'vi{sxf}{xfrac_new}({slr}),{r}={mystr}') + print() + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrix = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrix) +print_coef_formula(row_matrix,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrix = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrix) +print_coef_formula(row_matrix,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +#print_coef_formula(mymat3R,-xfrac,1) +print_coef_formula(mymat3R,-xfrac) + diff --git a/example/figure/1d/weno/interplate/xi/07c/xi.py b/example/figure/1d/weno/interplate/xi/07c/xi.py new file mode 100644 index 00000000..6b5f322d --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07c/xi.py @@ -0,0 +1,270 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + if rows == 1: + r = rin + for j in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = ishift - r + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + #print(f'vi{sxf}{xfrac_new}({slr}),r={r}={mystr}') + print(f'vi{sxf}{xfrac_new}({slr}),{r}={mystr}') + print() + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + print(f'rj_dict={rj_dict}') + print(sorted(rj_dict.items())) + for rj, pairs in sorted(rj_dict.items()): + print(rj, pairs) + for rj, pairs in sorted(rj_dict.items()): + readable = [(idx, str(frac)) for idx, frac in pairs] + print(rj, readable) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07d/xi.py b/example/figure/1d/weno/interplate/xi/07d/xi.py new file mode 100644 index 00000000..a9380aa1 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07d/xi.py @@ -0,0 +1,264 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + for i in range(rows): + mystr = '' + r = i + if rows == 1: + r = rin + for j in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = ishift - r + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + print(f'vi{sxf}{xfrac_new}({slr}),{r}={mystr}') + print() + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + #print(f'rj_dict={rj_dict}') + #print(sorted(rj_dict.items())) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07e/xi.py b/example/figure/1d/weno/interplate/xi/07e/xi.py new file mode 100644 index 00000000..3d47d9e5 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07e/xi.py @@ -0,0 +1,267 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + vstr = f'vi{sxf}{xfrac_new}({slr})' + for i in range(rows): + r = i + row = alist[i] + if rows == 1: + r = rin + mystr = '' + jstart = ishift - r + for j in range(cols): + absv,ss = coef_toabsstring(row[j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = jstart + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + print(f'{vstr},{r}={mystr}') + print() + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + #print(f'rj_dict={rj_dict}') + #print(sorted(rj_dict.items())) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07f/xi.py b/example/figure/1d/weno/interplate/xi/07f/xi.py new file mode 100644 index 00000000..19a6ecb5 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07f/xi.py @@ -0,0 +1,274 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + vstr = f'vi{sxf}{xfrac_new}({slr})' + for i in range(rows): + r = i + row = alist[i] + if rows == 1: + r = rin + mystr = '' + jstart = ishift - r + var_rj_list = cal_iplus_index(jstart,cols) + for j in range(cols): + absv,ss = coef_toabsstring(row[j]) + if j == 0 and ss == '+': + ss = ' ' + ss = f"{ss}{absv:>{widths[j]-1}}" + mystr += ss + f"*v[i{var_rj_list[j]}]" + print(f'{vstr},{r}={mystr}') + print() + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + #print(f'rj_dict={rj_dict}') + #print(sorted(rj_dict.items())) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07g/xi.py b/example/figure/1d/weno/interplate/xi/07g/xi.py new file mode 100644 index 00000000..0e186ef4 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07g/xi.py @@ -0,0 +1,326 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac, shift, row_index): + """ + 构建左边标签,如 'vi+1/2(-),0' + x_frac: 分数部分(如 1/2),建议传入字符串 '1/2' + shift: 整体偏移 + row_index: 当前行号(用于 ,0, ,1, ,2) + """ + total_offset = x_frac + shift + sign_char = '+' if total_offset >= 0 else '' + # 注意:x_frac 是字符串,如 '1/2',所以 total_offset 需为字符串 + # 为简化,假设 x_frac 是字符串(如 '1/2'),shift 是整数 + if shift == 0: + offset_str = f"+{x_frac}" if total_offset >= 0 else f"{x_frac}" + else: + # 更健壮的做法:让调用者传入完整偏移字符串 + offset_str = f"{sign_char}{total_offset}" # 需要重新设计此处逻辑 + + # 简化:假设 x_frac 是字符串(如 '1/2'),整体偏移用 shift 控制 + # 实际中,建议将 x_frac 设计为字符串,如 "1/2" + lhs_base = f"vi+{x_frac}" if shift == 0 else f"vi{total_offset}" + lr_sign = '-' if x_frac.startswith('-') or (isinstance(x_frac, str) and x_frac[0].isdigit()) else '+' + # 但根据你原始逻辑:当 xfrac < 0 时 slr='+', 否则 '-' + # 所以更准确的是: + lr_sign = '-' if x_frac >= 0 else '+' + return f"vi{'+' if total_offset >= 0 else ''}{total_offset}({lr_sign})" + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + vstr = f'vi{sxf}{xfrac_new}({slr})' + for i in range(rows): + r = i + row = alist[i] + if rows == 1: + r = rin + mystr = '' + jstart = ishift - r + var_rj_list = cal_iplus_index(jstart,cols) + for j in range(cols): + ss = format_signed_coef(row[j],j==0,widths[j]) + ioffset_str = build_variable_index_string(jstart + j) + print(f'ioffset_str={ioffset_str}') + mystr += ss + f"*v{ioffset_str}" + print(f'{vstr},{r}={mystr}') + print() + + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + #print(f'rj_dict={rj_dict}') + #print(sorted(rj_dict.items())) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07h/xi.py b/example/figure/1d/weno/interplate/xi/07h/xi.py new file mode 100644 index 00000000..cc42d5cd --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07h/xi.py @@ -0,0 +1,338 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + vstr = f'vi{sxf}{xfrac_new}({slr})' + print(f'vstr={vstr}') + lhs_label = build_lhs_label(xfrac, ishift) + print(f'lhs_label={lhs_label}') + for i in range(rows): + r = i + row = alist[i] + if rows == 1: + r = rin + mystr = '' + jstart = ishift - r + ioffset_strs = build_variable_indices(jstart, cols) + for j in range(cols): + ss = format_signed_coef(row[j],j==0,widths[j]) + mystr += ss + f"*v{ioffset_strs[j]}" + print(f'{vstr},{r}={mystr}') + print() + + # 示例 1: x_frac = 1/2, shift = 0 + print(build_lhs_label(Fraction(1, 2), 0)) + # 输出: vi+1/2(-) + + # 示例 2: x_frac = -1/2, shift = 0 + print(build_lhs_label(Fraction(-1, 2), 0)) + # 输出: vi-1/2(+) + + # 示例 3: x_frac = 1/2, shift = -1 + print(build_lhs_label(Fraction(1, 2), -1)) + # total_offset = -1 + 1/2 = -1/2 → "vi-1/2(-)" + # 注意:方向仍由 x_frac=1/2≥0 → '-' + # 输出: vi-1/2(-) + + # 示例 4: x_frac = -3/2, shift = 2 + print(build_lhs_label(Fraction(-3, 2), 2)) + # total_offset = 2 - 3/2 = 1/2 → "vi+1/2" + # direction: x_frac < 0 → '+' + # 输出: vi+1/2(+) + + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + #print(f'rj_dict={rj_dict}') + #print(sorted(rj_dict.items())) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07i/xi.py b/example/figure/1d/weno/interplate/xi/07i/xi.py new file mode 100644 index 00000000..3555e169 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07i/xi.py @@ -0,0 +1,318 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def print_coef_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + terms = [] + jstart = ishift - r + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + print(f'{lhs_label},{r}={rhs_label}') + + print() + + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + #print(f'rj_dict={rj_dict}') + #print(sorted(rj_dict.items())) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07j/xi.py b/example/figure/1d/weno/interplate/xi/07j/xi.py new file mode 100644 index 00000000..c6806185 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07j/xi.py @@ -0,0 +1,374 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + + +def printhhh(row_matrix, mymat, rbase, xfrac): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + + print_matrix_fraction(row_matrix) + print_matrix_fraction(mymat) + + print(f'rbase={rbase}') + + print_stencil_formula(row_matrix,xfrac,0,rbase) + + print_stencil_formula(mymat,xfrac) + + """ + rows_ref,cols_ref=1,5 + rows,cols=3,3 + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + rbase=2 + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + rj_set={0, 1, 2, -2, -1} + sorted_rj_set=[-2, -1, 0, 1, 2] + -2 [(2, 1/3)] + -1 [(1, -1/6), (2, -7/6)] + 0 [(0, 1/3), (1, 5/6), (2, 11/6)] + 1 [(0, 5/6), (1, 1/3)] + 2 [(0, -1/6)] + vi+1/2(-)=d[0]*( 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2]) + +d[1]*(-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1]) + +d[2]*( 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ]) + -2 [(2, 1/3)] 表示下标-2也就是v[i-2]对应的系数为d[2]*1/3 + 这个系数应该和vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + 里面的v[i-2]系数一致,也就是d[2]*1/3v[i-2]=1/30v[i-2],从而求出d[2],同理可以求出d[0], + 最后求出d[1],我的想法是一步步搞清楚到底该怎么做,在思维不清晰之前先使用代码将各项表达式对应打印出来, + 第一步-2 [(2, 1/3)]已经做到了,但是不直观。第二步应该将其变化为 + v[i-2]: [(d[2]*1/3)=1/30] + v[i-1]: [(d[1]*(-1/6)+d[2]*( -7/6)=-13/60],以此类推。你有什么建议,并给出代码以便于进一步讨论。 + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + +def build_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def testfun(mymat): + sub_stencils = mymat + target_dict = { + -2: Fraction(1, 30), + -1: Fraction(-13, 60), + 0: Fraction(47, 60), + 1: Fraction(9, 20), + 2: Fraction(-1, 20) + } + print_weno_equations(sub_stencils, target_dict) + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_stencil_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_stencil_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_stencil_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_stencil_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_stencil_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_stencil_formula(mymat3R,-xfrac) + +#printhhh(row_matrixL, mymat3L, r, xfrac) + +testfun(mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/08/xi.py b/example/figure/1d/weno/interplate/xi/08/xi.py new file mode 100644 index 00000000..4277ea57 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08/xi.py @@ -0,0 +1,392 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + + +def printhhh(row_matrix, mymat, rbase, xfrac): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + + print_matrix_fraction(row_matrix) + print_matrix_fraction(mymat) + + print(f'rbase={rbase}') + + print_stencil_formula(row_matrix,xfrac,0,rbase) + + print_stencil_formula(mymat,xfrac) + + """ + rows_ref,cols_ref=1,5 + rows,cols=3,3 + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + rbase=2 + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + rj_set={0, 1, 2, -2, -1} + sorted_rj_set=[-2, -1, 0, 1, 2] + -2 [(2, 1/3)] + -1 [(1, -1/6), (2, -7/6)] + 0 [(0, 1/3), (1, 5/6), (2, 11/6)] + 1 [(0, 5/6), (1, 1/3)] + 2 [(0, -1/6)] + vi+1/2(-)=d[0]*( 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2]) + +d[1]*(-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1]) + +d[2]*( 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ]) + -2 [(2, 1/3)] 表示下标-2也就是v[i-2]对应的系数为d[2]*1/3 + 这个系数应该和vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + 里面的v[i-2]系数一致,也就是d[2]*1/3v[i-2]=1/30v[i-2],从而求出d[2],同理可以求出d[0], + 最后求出d[1],我的想法是一步步搞清楚到底该怎么做,在思维不清晰之前先使用代码将各项表达式对应打印出来, + 第一步-2 [(2, 1/3)]已经做到了,但是不直观。第二步应该将其变化为 + v[i-2]: [(d[2]*1/3)=1/30] + v[i-1]: [(d[1]*(-1/6)+d[2]*( -7/6)=-13/60],以此类推。你有什么建议,并给出代码以便于进一步讨论。 + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + +def build_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for i in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[i, j] + offset_map[k].append(coef) + return offset_map + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_offset_map(sub_stencils) + print(f'offset_map={offset_map}') + all_offsets = sorted(target_dict.keys()) + print(f'all_offsets={all_offsets}') + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def testfun(row_matrix, mymat, r): + sub_stencils = mymat + target_dict = { + -2: Fraction(1, 30), + -1: Fraction(-13, 60), + 0: Fraction(47, 60), + 1: Fraction(9, 20), + 2: Fraction(-1, 20) + } + + offset_map = build_offset_map_r(row_matrixL,2) + print(f'offset_map={offset_map}') + print(f'target_dict={target_dict}') + + print_weno_equations(sub_stencils, target_dict) + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_stencil_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_stencil_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_stencil_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_stencil_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_stencil_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_stencil_formula(mymat3R,-xfrac) + +#printhhh(row_matrixL, mymat3L, r, xfrac) + +testfun(row_matrixL, mymat3L, r) diff --git a/example/figure/1d/weno/interplate/xi/08a/xi.py b/example/figure/1d/weno/interplate/xi/08a/xi.py new file mode 100644 index 00000000..d128adb4 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08a/xi.py @@ -0,0 +1,440 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + + +def printhhh(row_matrix, mymat, rbase, xfrac): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + + print_matrix_fraction(row_matrix) + print_matrix_fraction(mymat) + + print(f'rbase={rbase}') + + print_stencil_formula(row_matrix,xfrac,0,rbase) + + print_stencil_formula(mymat,xfrac) + + """ + rows_ref,cols_ref=1,5 + rows,cols=3,3 + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + rbase=2 + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + rj_set={0, 1, 2, -2, -1} + sorted_rj_set=[-2, -1, 0, 1, 2] + -2 [(2, 1/3)] + -1 [(1, -1/6), (2, -7/6)] + 0 [(0, 1/3), (1, 5/6), (2, 11/6)] + 1 [(0, 5/6), (1, 1/3)] + 2 [(0, -1/6)] + vi+1/2(-)=d[0]*( 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2]) + +d[1]*(-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1]) + +d[2]*( 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ]) + -2 [(2, 1/3)] 表示下标-2也就是v[i-2]对应的系数为d[2]*1/3 + 这个系数应该和vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + 里面的v[i-2]系数一致,也就是d[2]*1/3v[i-2]=1/30v[i-2],从而求出d[2],同理可以求出d[0], + 最后求出d[1],我的想法是一步步搞清楚到底该怎么做,在思维不清晰之前先使用代码将各项表达式对应打印出来, + 第一步-2 [(2, 1/3)]已经做到了,但是不直观。第二步应该将其变化为 + v[i-2]: [(d[2]*1/3)=1/30] + v[i-1]: [(d[1]*(-1/6)+d[2]*( -7/6)=-13/60],以此类推。你有什么建议,并给出代码以便于进一步讨论。 + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for i in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[i, j] + #offset_map[k].append(coef) + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row, base_offset=-2): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def testfun(row_matrix, mymat, r): + sub_stencils = mymat + target_dict = build_substencil_offset_map_r(row_matrix,2) + print_weno_equations(sub_stencils, target_dict) + + my_row_matrix = np.array([[ + Fraction(1,30), Fraction(-13,60), Fraction(47,60), Fraction(9,20), Fraction(-1,20) + ]], dtype=object) + + # Build target map + target_map = build_target_offset_map(row_matrix[0], base_offset=-2) + + # Solve + weights = solve_weno_weights(mymat, target_map) + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +#print_matrix_fraction(mymat5L) +#print_stencil_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +#print_matrix_fraction(row_matrixL) +#print_stencil_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +#print_matrix_fraction(mymat5R) +#print_stencil_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +#print_matrix_fraction(row_matrixR) +#print_stencil_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +#print_matrix_fraction(mymat3L) +#print_stencil_formula(mymat3L,xfrac) + +#print_matrix_fraction(mymat3R) +#print_stencil_formula(mymat3R,-xfrac) + +testfun(row_matrixL, mymat3L, r) +testfun(row_matrixR, mymat3R, r) diff --git a/example/figure/1d/weno/interplate/xi/08b/xi.py b/example/figure/1d/weno/interplate/xi/08b/xi.py new file mode 100644 index 00000000..17a24160 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08b/xi.py @@ -0,0 +1,506 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for i in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[i, j] + #offset_map[k].append(coef) + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row, base_offset=-2): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat, r): + sub_stencils = mymat + target_dict = build_substencil_offset_map_r(row_matrix,r) + print_weno_equations(sub_stencils, target_dict) + + # Build target map + target_map = build_target_offset_map(row_matrix[0], base_offset=-r) + + # Solve + weights = solve_weno_weights(mymat, target_map) + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +#print_matrix_fraction(mymat5L) +#print_stencil_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +#print_matrix_fraction(row_matrixL) +#print_stencil_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +#print_matrix_fraction(mymat5R) +#print_stencil_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +#print_matrix_fraction(row_matrixR) +#print_stencil_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print() +print_matrix_fraction(mymat3R) +#print_stencil_formula(mymat3L,xfrac) + +#print_matrix_fraction(mymat3R) +#print_stencil_formula(mymat3R,-xfrac) + +compute_weno_linear_weights(row_matrixL, mymat3L, r) +compute_weno_linear_weights(row_matrixR, mymat3R, r) + +mymat3L = generate_left_stencils(3) +mymat3R = generate_right_stencils(3) + +print_matrix_fraction(mymat3L) +print() +print_matrix_fraction(mymat3R) diff --git a/example/figure/1d/weno/interplate/xi/08c/xi.py b/example/figure/1d/weno/interplate/xi/08c/xi.py new file mode 100644 index 00000000..6a6baea5 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08c/xi.py @@ -0,0 +1,502 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + print(f'r={r}') + print(f'sub_stencils={sub_stencils}') + rows, cols = sub_stencils.shape + print(f'rows,cols={rows},{cols}') + offset_map = defaultdict(list) + for i in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[i, j] + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row, base_offset=-2): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat, r): + sub_stencils = mymat + target_dict = build_substencil_offset_map_r(row_matrix,r) + print_weno_equations(sub_stencils, target_dict) + + # Build target map + target_map = build_target_offset_map(row_matrix[0], base_offset=-r) + + # Solve + weights = solve_weno_weights(mymat, target_map) + + +xfrac = Fraction(1,2) +k5 = 5 +mymat5L = generate_reconstruction_stencils(k5, xfrac) +mymat5R = generate_reconstruction_stencils(k5, -xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +row_matrixR = mymat5R[r, :].reshape(1, -1) + +print_matrix_fraction(mymat5L) +print_matrix_fraction(mymat5R) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(row_matrixR) + +row_matL = compute_stencil_coefficients_for_point(r, k5, xfrac) +row_matR = compute_stencil_coefficients_for_point(r, k5, -xfrac) +print_matrix_fraction(row_matL) +print_matrix_fraction(row_matR) + + +k3=3 +mymat3L = generate_left_stencils(k3) +mymat3R = generate_right_stencils(k3) + +print_matrix_fraction(mymat3L) +print() +print_matrix_fraction(mymat3R) +print_stencil_formula(mymat3L,xfrac) + +compute_weno_linear_weights(row_matrixL, mymat3L, r) +compute_weno_linear_weights(row_matrixR, mymat3R, r) + diff --git a/example/figure/1d/weno/interplate/xi/08d/xi.py b/example/figure/1d/weno/interplate/xi/08d/xi.py new file mode 100644 index 00000000..cd09a0fc --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08d/xi.py @@ -0,0 +1,507 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for i in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[i, j] + #offset_map[k].append(coef) + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row, base_offset=-2): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat, r): + sub_stencils = mymat + target_dict = build_substencil_offset_map_r(row_matrix,r) + print(f'target_dict={target_dict}') + print_weno_equations(sub_stencils, target_dict) + + # Build target map + target_map = build_target_offset_map(row_matrix[0], base_offset=-r) + + # Solve + weights = solve_weno_weights(mymat, target_map) + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +#print_matrix_fraction(mymat5L) +#print_stencil_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +#print_matrix_fraction(row_matrixL) +#print_stencil_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +#print_matrix_fraction(mymat5R) +#print_stencil_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +#print_matrix_fraction(row_matrixR) +#print_stencil_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print() +print_matrix_fraction(mymat3R) +#print_stencil_formula(mymat3L,xfrac) + +#print_matrix_fraction(mymat3R) +#print_stencil_formula(mymat3R,-xfrac) + +compute_weno_linear_weights(row_matrixL, mymat3L, r) +compute_weno_linear_weights(row_matrixR, mymat3R, r) + +mymat3L = generate_left_stencils(3) +mymat3R = generate_right_stencils(3) + +print_matrix_fraction(mymat3L) +print() +print_matrix_fraction(mymat3R) diff --git a/example/figure/1d/weno/interplate/xi/08e/xi.py b/example/figure/1d/weno/interplate/xi/08e/xi.py new file mode 100644 index 00000000..28b9cd1c --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08e/xi.py @@ -0,0 +1,516 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + print(f'r={r}') + print(f'type(sub_stencils)={type(sub_stencils)}') + print(f'sub_stencils={sub_stencils}') + print(f'sub_stencils.shape={sub_stencils.shape}') + cols = sub_stencils.shape[0] + print(f'cols={cols}') + offset_map = defaultdict(list) + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[j] + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print(f'target_dict={target_dict}') + print_weno_equations(sub_stencils, target_dict) + + #print(f'target_dict={target_dict}') + + # Solve + weights = solve_weno_weights(mymat, target_dict) + + +xfrac = Fraction(1,2) +k5 = 5 + +row_matL = compute_optimal_reconstruction_stencil(k5, xfrac) +row_matR = compute_optimal_reconstruction_stencil(k5, -xfrac) + +print_matrix_fraction(row_matL) +print_matrix_fraction(row_matR) + +k3=3 +mymat3L = generate_left_stencils(k3) +mymat3R = generate_right_stencils(k3) + +print_matrix_fraction(mymat3L) +print() +print_matrix_fraction(mymat3R) + +compute_weno_linear_weights(row_matL, mymat3L) +compute_weno_linear_weights(row_matR, mymat3R) + diff --git a/example/figure/1d/weno/interplate/xi/08f/xi.py b/example/figure/1d/weno/interplate/xi/08f/xi.py new file mode 100644 index 00000000..c1f87b55 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08f/xi.py @@ -0,0 +1,510 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + print(f'r={r}') + print(f'type(sub_stencils)={type(sub_stencils)}') + print(f'sub_stencils={sub_stencils}') + print(f'sub_stencils.shape={sub_stencils.shape}') + cols = sub_stencils.shape[0] + print(f'cols={cols}') + offset_map = defaultdict(list) + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[j] + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + #print(f'target_dict={target_dict}') + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymat3L = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymat3L) + + mymat3R = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymat3R) + +compute_weno_linear_weights_new(3) + diff --git a/example/figure/1d/weno/interplate/xi/08g/xi.py b/example/figure/1d/weno/interplate/xi/08g/xi.py new file mode 100644 index 00000000..ad125717 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08g/xi.py @@ -0,0 +1,520 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + print(f'r={r}') + print(f'type(sub_stencils)={type(sub_stencils)}') + print(f'sub_stencils={sub_stencils}') + print(f'sub_stencils.shape={sub_stencils.shape}') + cols = sub_stencils.shape[0] + print(f'cols={cols}') + offset_map = defaultdict(list) + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[j] + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + #print(f'sub_stencils={sub_stencils}') + #print(f'all_offsets={all_offsets}') + #print(f'target_dict={target_dict}') + + rows, cols = sub_stencils.shape + #print(f'rows, cols={rows},{cols}') + + #print("WENO linear system (for weights d[0], d[1], d[2]):\n") + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + #print(f'target_dict={target_dict}') + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +compute_weno_linear_weights_new(3) + diff --git a/example/figure/1d/weno/interplate/xi/09/xi.py b/example/figure/1d/weno/interplate/xi/09/xi.py new file mode 100644 index 00000000..32ecfb33 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/09/xi.py @@ -0,0 +1,521 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + print(f'r={r}') + print(f'type(sub_stencils)={type(sub_stencils)}') + print(f'sub_stencils={sub_stencils}') + print(f'sub_stencils.shape={sub_stencils.shape}') + cols = sub_stencils.shape[0] + print(f'cols={cols}') + offset_map = defaultdict(list) + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[j] + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + #print(f'sub_stencils={sub_stencils}') + #print(f'all_offsets={all_offsets}') + #print(f'target_dict={target_dict}') + + rows, cols = sub_stencils.shape + #print(f'rows, cols={rows},{cols}') + + #print("WENO linear system (for weights d[0], d[1], d[2]):\n") + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +compute_weno_linear_weights_new(1) +compute_weno_linear_weights_new(2) +compute_weno_linear_weights_new(3) + + diff --git a/example/figure/1d/weno/interplate/xi/09a/xi.py b/example/figure/1d/weno/interplate/xi/09a/xi.py new file mode 100644 index 00000000..8d5b2480 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/09a/xi.py @@ -0,0 +1,569 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_weno_substencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + print(f'r={r}') + print(f'type(sub_stencils)={type(sub_stencils)}') + print(f'sub_stencils={sub_stencils}') + print(f'sub_stencils.shape={sub_stencils.shape}') + cols = sub_stencils.shape[0] + print(f'cols={cols}') + offset_map = defaultdict(list) + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[j] + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + #print(f'sub_stencils={sub_stencils}') + #print(f'all_offsets={all_offsets}') + #print(f'target_dict={target_dict}') + + rows, cols = sub_stencils.shape + #print(f'rows, cols={rows},{cols}') + + #print("WENO linear system (for weights d[0], d[1], d[2]):\n") + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +def solve_weno_linear_weights(optimal_stencil: np.ndarray, sub_stencils: np.ndarray) -> np.ndarray: + """ + Solve for linear weights d such that: + optimal_stencil ≈ sum_j d[j] * sub_stencils[j] + + Prints the linear system and solved weights. + """ + + # Build target map + target_dict = build_target_offset_map(optimal_stencil) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(sub_stencils, target_dict) + return weights + +def demo_weno_linear_weights(weno_r: int): + """ + Demonstrate linear weight computation for WENO-r scheme. + + Parameters: + weno_r (int): Number of substencils (e.g., 3 for WENO5, 2 for WENO3) + """ + x_half = Fraction(1, 2) + global_stencil_width = 2 * weno_r - 1 # e.g., 5 for WENO3 + + # Left-biased (v_{i+1/2}^-) + substencils_L = generate_weno_substencils(stencil_width=weno_r, x_point=x_half) + optimal_L = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=x_half + ) + weights_L = solve_weno_linear_weights(optimal_L, substencils_L) + + # Right-biased (v_{i-1/2}^+) + substencils_R = generate_weno_substencils(stencil_width=weno_r, x_point=-x_half) + optimal_R = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=-x_half + ) + weights_R = solve_weno_linear_weights(optimal_R, substencils_R) + + return weights_L, weights_R + + +# WENO3 (2 substencils → 3rd-order) +demo_weno_linear_weights(weno_r=2) + +# WENO5 (3 substencils → 5th-order) +demo_weno_linear_weights(weno_r=3) + +# WENO7 (4 substencils → 7th-order) +demo_weno_linear_weights(weno_r=4) + + diff --git a/example/figure/1d/weno/interplate/xi/09b/xi.py b/example/figure/1d/weno/interplate/xi/09b/xi.py new file mode 100644 index 00000000..030be928 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/09b/xi.py @@ -0,0 +1,542 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_weno_substencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + rows, cols = sub_stencils.shape + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +def solve_weno_linear_weights(optimal_stencil: np.ndarray, sub_stencils: np.ndarray) -> np.ndarray: + """ + Solve for linear weights d such that: + optimal_stencil ≈ sum_j d[j] * sub_stencils[j] + + Prints the linear system and solved weights. + """ + + # Build target map + target_dict = build_target_offset_map(optimal_stencil) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(sub_stencils, target_dict) + return weights + +def demo_weno_linear_weights(weno_r: int): + """ + Demonstrate linear weight computation for WENO-r scheme. + + Parameters: + weno_r (int): Number of substencils (e.g., 3 for WENO5, 2 for WENO3) + """ + x_half = Fraction(1, 2) + global_stencil_width = 2 * weno_r - 1 # e.g., 5 for WENO3 + + # Left-biased (v_{i+1/2}^-) + substencils_L = generate_weno_substencils(stencil_width=weno_r, x_point=x_half) + optimal_L = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=x_half + ) + weights_L = solve_weno_linear_weights(optimal_L, substencils_L) + + # Right-biased (v_{i-1/2}^+) + substencils_R = generate_weno_substencils(stencil_width=weno_r, x_point=-x_half) + optimal_R = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=-x_half + ) + weights_R = solve_weno_linear_weights(optimal_R, substencils_R) + + return weights_L, weights_R + +if __name__ == "__main__": + maxk = 3 + for k in range(1,maxk+1): + print(f"\n=== WENO{2*k-1} ===") + demo_weno_linear_weights(weno_r=k) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/01/matrix.py b/example/figure/1d/weno/matrix/01/matrix.py new file mode 100644 index 00000000..f50de4db --- /dev/null +++ b/example/figure/1d/weno/matrix/01/matrix.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" +使用 SymPy 生成并操作下面的矩矩阵 + +M_{j,i} = ∫_{α_j}^{β_j} ξ^i dξ, +α_j = -r + j - 1/2, +β_j = -r + j + 1/2, +j,i = 0,…,k-1 +""" + +import sympy as sp + +def matrix_element(i, j, r): + """返回 M_{j,i} = (β_j^{i+1} - α_j^{i+1})/(i+1)""" + half = sp.Rational(1, 2) + alpha = -r + j - half + beta = -r + j + half + return sp.simplify((beta**(i+1) - alpha**(i+1)) / (i+1)) + +def moment_matrix(k_val, r_sym): + """生成 k×k 的矩矩阵 M""" + M = sp.zeros(k_val, k_val) + for j in range(k_val): + for i in range(k_val): + M[j, i] = matrix_element(i, j, r_sym) + return M + +# ------------------- 示例 ------------------- +if __name__ == "__main__": + # 符号参数 + r = sp.symbols('r', real=True) + + # 1) 生成 3×3 矩阵并打印 + M3 = moment_matrix(3, r) + print("3×3 矩矩阵 M (符号 r):") + sp.pprint(M3) + + # 2) 取 r = 2 得到数值矩阵 + M3_num = M3.subs(r, 2) + print("\n当 r = 2 时的数值矩阵:") + sp.pprint(M3_num) + + # 3) 计算行列式 + det_M3 = sp.simplify(M3.det()) + print("\n3×3 矩阵的行列式 (化简后):") + sp.pprint(det_M3) + + # 4) 生成 4×4 矩阵并查看行列式 + M4 = moment_matrix(4, r) + print("\n4×4 矩矩阵 M (符号 r):") + sp.pprint(M4) + det_M4 = sp.simplify(M4.det()) + print("\n4×4 矩阵的行列式 (化简后):") + sp.pprint(det_M4) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/01a/matrix.py b/example/figure/1d/weno/matrix/01a/matrix.py new file mode 100644 index 00000000..671e8d50 --- /dev/null +++ b/example/figure/1d/weno/matrix/01a/matrix.py @@ -0,0 +1,28 @@ +import sympy as sp + +def moment_matrix_k3(r_val): + """生成 k=3 时的矩阵,并返回 LaTeX 字符串""" + k = 3 + r = sp.symbols('r') + half = sp.Rational(1, 2) + + # 创建矩阵 + M = sp.zeros(k, k) + for j in range(k): + for i in range(k): + alpha = -r + j - half + beta = -r + j + half + M[j, i] = (beta**(i+1) - alpha**(i+1)) / (i+1) + + # 代入具体的 r 值 + M_substituted = M.subs(r, r_val) + + # 转换为 LaTeX,使用简化模式 + latex_code = sp.latex(M_substituted, mat_str='matrix', mat_delim='[') + return latex_code + +# 生成 r = 0, 1, 2 的 LaTeX 代码 +for r in [0, 1, 2]: + latex = moment_matrix_k3(r) + print(f"\n%% 当 r = {r} 时的矩阵 (k=3):") + print(latex) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/01b/matrix.py b/example/figure/1d/weno/matrix/01b/matrix.py new file mode 100644 index 00000000..fed93488 --- /dev/null +++ b/example/figure/1d/weno/matrix/01b/matrix.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" +计算 c = φᵀ(½)·M⁻¹,其中 + M_{j,i}=∫_{α_j}^{β_j} ξ^i dξ, + α_j = -r + j - ½, β_j = -r + j + ½, + φᵀ(½) = [1, ½, …, (½)^{k-1}]. + +对 r = 0,1,2 计算对应的行向量 c,组合成矩阵 C 并输出 LaTeX。 +""" + +import sympy as sp + +def moment_matrix(k_val, r_sym): + """生成 k×k 的矩矩阵 M(符号形式)""" + half = sp.Rational(1, 2) + M = sp.zeros(k_val, k_val) + for j in range(k_val): + alpha = -r_sym + j - half + beta = -r_sym + j + half + for i in range(k_val): + M[j, i] = (beta**(i + 1) - alpha**(i + 1)) / (i + 1) + return M + +def compute_c_matrix(k_val): + """ + 计算 C 矩阵(3×k),其每一行对应 r = 0,1,2 时的 c = φᵀ(½)·M⁻¹ + """ + # 把 r 视为符号,后面再代入具体数值 + r = sp.symbols('r', real=True) + + # 基础的符号矩阵 M + M_sym = moment_matrix(k_val, r) + + # φᵀ(½) 行向量 + phi_T = sp.Matrix([ (sp.Rational(1, 2))**i for i in range(k_val) ]).T + + rows = [] + for r_val in [0, 1, 2]: + # 代入当前 r,得到数值矩阵并求逆 + M_num = M_sym.subs(r, r_val) + M_inv = M_num.inv() + # c = φᵀ(½)·M⁻¹ + c_row = phi_T * M_inv + rows.append(c_row) + + # 纵向堆叠成 3×k 矩阵 + C = sp.Matrix(rows) + return C + +def latex_matrix(mat, env='bmatrix'): + """将 sympy 矩阵转为 LaTeX 字符串""" + return sp.latex(mat, mat_str=env, mat_delim='[') + +# ------------------- 示例 ------------------- +if __name__ == "__main__": + # k = 2 + C2 = compute_c_matrix(2) + print("%% k = 2 时的 C 矩阵(LaTeX)") + print(latex_matrix(C2)) + + # k = 3 + C3 = compute_c_matrix(3) + print("\n%% k = 3 时的 C 矩阵(LaTeX)") + print(latex_matrix(C3)) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/01c/matrix.py b/example/figure/1d/weno/matrix/01c/matrix.py new file mode 100644 index 00000000..dd0edeb1 --- /dev/null +++ b/example/figure/1d/weno/matrix/01c/matrix.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +""" +计算重构系数矩阵:coeff_matrix = φᵀ(ξ₀)·M⁻¹ +其中 M 为矩矩阵,ξ₀ 为指定位置(如 ±1/2) +""" + +import sympy as sp + +def moment_matrix(k_val, r_sym): + """生成 k×k 的矩矩阵 M(符号形式)""" + half = sp.Rational(1, 2) + M = sp.zeros(k_val, k_val) + for j in range(k_val): + alpha = -r_sym + j - half + beta = -r_sym + j + half + for i in range(k_val): + M[j, i] = (beta**(i + 1) - alpha**(i + 1)) / (i + 1) + return M + +def compute_coeff_matrix(k_val, xi0=sp.Rational(1, 2)): + """ + 计算系数矩阵(3×k),每行对应 r = 0,1,2 时的 φᵀ(ξ₀)·M⁻¹ + + 参数 + ---------- + k_val : int + 矩阵阶数 + xi0 : sympy.Rational + 求值位置(默认为 1/2) + """ + r = sp.symbols('r', real=True) + M_sym = moment_matrix(k_val, r) + phi_T = sp.Matrix([xi0**i for i in range(k_val)]).T # φᵀ(ξ₀) + #print(f"phi_T={phi_T}") + #print(sp.latex(phi_T, mat_str='matrix', mat_delim='[')) + + rows = [] + for r_val in [0, 1, 2]: + M_inv = M_sym.subs(r, r_val).inv() + c_row = phi_T * M_inv + rows.append(c_row) + + coeff_matrix = sp.Matrix(rows) # 推荐变量名 + return coeff_matrix + +def latex_matrix(mat, env='matrix'): + """将 sympy 矩阵转为 LaTeX 字符串""" + return sp.latex(mat, mat_str=env, mat_delim='[') + +# ------------------- 示例 ------------------- +if __name__ == "__main__": + # 默认 ξ₀ = 1/2 + print("%% 当 ξ₀ = 1/2 时:") + + C2 = compute_coeff_matrix(2) + print("\n%% k = 2 的系数矩阵:") + print(latex_matrix(C2)) + + C3 = compute_coeff_matrix(3) + print("\n%% k = 3 的系数矩阵:") + print(latex_matrix(C3)) + + # 示例:ξ₀ = -1/2 + print("\n\n%% 当 ξ₀ = -1/2 时:") + + C2_neg = compute_coeff_matrix(2, xi0=sp.Rational(-1, 2)) + print("\n%% k = 2 的系数矩阵:") + print(latex_matrix(C2_neg)) + + C3_neg = compute_coeff_matrix(3, xi0=sp.Rational(-1, 2)) + print("\n%% k = 3 的系数矩阵:") + print(latex_matrix(C3_neg)) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02/matrix.py b/example/figure/1d/weno/matrix/02/matrix.py new file mode 100644 index 00000000..c736a777 --- /dev/null +++ b/example/figure/1d/weno/matrix/02/matrix.py @@ -0,0 +1,87 @@ +import sympy as sp + +def construct_system(k, r_symbolic=True, specific_r=None): + """ + 构造积分约束系统:∫_{α}^{β} p(r, φ) dφ = v̄_{i−r+j} + + 参数: + k (int): 多项式阶数 (p 是 k−1 次多项式) + r_symbolic (bool): 是否将 r 保留为符号(True)还是使用 specific_r(False) + specific_r (int): 若 r_symbolic=False,指定具体的 r 值 (0 ≤ r ≤ k−1) + + 返回: + A (Matrix): k×k 系数矩阵 A_{j,m} = ∫ φ^m dφ over [α(r,j), β(r,j)] + rhs_symbols (list): 右端符号 [v̄_{i−r}, v̄_{i−r+1}, ..., v̄_{i−r+k−1}] + phi (Symbol): 积分变量(内部用 phi,LaTeX 可替换) + r (Symbol or int): 使用的 r + """ + # 积分变量(内部用 phi,LaTeX 显示为 \phi) + phi = sp.Symbol('phi') + + if r_symbolic: + r = sp.Symbol('r', integer=True) + else: + if specific_r is None: + raise ValueError("specific_r must be provided if r_symbolic=False") + if not (0 <= specific_r <= k - 1): + raise ValueError(f"specific_r={specific_r} must be in [0, {k-1}]") + r = specific_r + + # 多项式系数 a_0(r), ..., a_{k-1}(r) + a = sp.symbols(f'a0:{k}', cls=sp.Function) + # 注意:这里 a0(r) 是函数形式,保留对 r 的依赖 + + # 构造多项式 p(r, phi) = Σ_{m=0}^{k-1} a_m(r) * phi^m + p = sum(a[m](r) * phi**m for m in range(k)) + + # 右端符号:v̄_{i - r + j} + v = sp.symbols(f'vbar0:{k}') # vbar0, vbar1, ..., vbar_{k-1} + rhs_symbols = [v[j] for j in range(k)] # 对应 j=0,...,k−1 + + # 构建系数矩阵 A:A[j, m] = ∫_{α}^{β} φ^m dφ + A = sp.zeros(k, k) + + for j in range(k): + # α(r,j) = -r + j - 1/2, β(r,j) = -r + j + 1/2 + alpha = -r + j - sp.Rational(1, 2) + beta = -r + j + sp.Rational(1, 2) + + for m in range(k): + # 积分 ∫ φ^m dφ = (β^{m+1} - α^{m+1}) / (m+1) + integral = (beta**(m + 1) - alpha**(m + 1)) / sp.Rational(m + 1) + A[j, m] = sp.simplify(integral) + + return A, rhs_symbols, phi, r, a + +# ----------------------------- +# 示例 1:通用表达式(k=3,r 为符号) +# ----------------------------- +print("=== 通用表达式 (k=3, r symbolic) ===") +A_gen, rhs_gen, phi, r_sym, a_sym = construct_system(k=3, r_symbolic=True) + +# 打印矩阵 A(LaTeX,用 \phi 替代 \xi) +# SymPy 默认用 phi,若原用 xi 则需替换,但这里直接定义为 phi +print("系数矩阵 A (k=3):") +sp.pprint(A_gen) +print("\nLaTeX (A):") +latex_A = sp.latex(A_gen) +latex_A_phi = latex_A.replace(r'\xi', r'\phi') # 虽然这里没用 xi,但保险起见 +print(latex_A_phi) + +# ----------------------------- +# 示例 2:具体 r=0, k=3 +# ----------------------------- +print("\n=== 具体情况 (k=3, r=0) ===") +A_r0, rhs_r0, phi, r_val, a_val = construct_system(k=3, r_symbolic=False, specific_r=0) +sp.pprint(A_r0) +print("\nLaTeX (A for r=0):") +print(sp.latex(A_r0)) + +# ----------------------------- +# 示例 3:保留接口以供将来代入 r 值 +# ----------------------------- +print("\n=== 从通用表达式代入 r=1 ===") +A_r1 = A_gen.subs(r_sym, 1) +sp.pprint(A_r1) +print("\nLaTeX (A for r=1 via subs):") +print(sp.latex(A_r1)) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02a/matrix.py b/example/figure/1d/weno/matrix/02a/matrix.py new file mode 100644 index 00000000..ae9f2b34 --- /dev/null +++ b/example/figure/1d/weno/matrix/02a/matrix.py @@ -0,0 +1,89 @@ +import sympy as sp +from sympy import symbols, Function, latex +from sympy.printing.latex import LatexPrinter + +# ---- Step 1: 定义基础符号(可全局配置) ---- +k = sp.Symbol('k', integer=True, positive=True) +r, j, xi = sp.symbols('r j \\xi') # 注意:xi 命名为 \xi 以便 LaTeX + +# 多项式系数:a_m(r) 作为函数 +a = Function('a') +# 为简化,我们用 a(0)(r), a(1)(r), ... 表示 a_0(r), a_1(r), ... +# 但 LaTeX 中希望显示为 a_0(r),需自定义打印 + +# 右端符号:定义一个带下标的符号生成器 +def vbar(index_expr): + # 使用 Dummy symbol with name containing LaTeX + name = f"\\overline{{v}}_{{{sp.latex(index_expr)}}}" + return sp.Symbol(name) + +# ---- Step 2: 构造多项式 p(r, xi) ---- +def build_polynomial(order, use_r_in_coeff=True): + terms = [] + for m in range(order): + if use_r_in_coeff: + coeff = a(m)(r) # a_m(r) + else: + coeff = sp.Symbol(f'a_{m}') # a_m(无 r 依赖) + terms.append(coeff * xi**m) + return sum(terms) + +# ---- Step 3: 构造积分限 α, β ---- +alpha = -r + j - sp.Rational(1, 2) +beta = -r + j + sp.Rational(1, 2) + +# ---- Step 4: 构造各个表达式(保持符号) ---- +# 多项式(带 r 依赖) +p_r_xi = build_polynomial(k, use_r_in_coeff=True) + +# 积分等式(抽象) +integral_eq_abstract = sp.Eq( + sp.Integral(p_r_xi, (xi, alpha, beta)), + vbar(sp.Symbol('i') - r + j) +) + +# 积分等式(显式展开,带 a_m(r)) +p_expanded = p_r_xi +integral_eq_expanded = sp.Eq( + sp.Integral(p_expanded, (xi, alpha, beta)), + vbar(sp.Symbol('i') - r + j) +) + +# 积分等式(a_m 不带 r) +p_no_r = build_polynomial(k, use_r_in_coeff=False) +integral_eq_no_r = sp.Eq( + sp.Integral(p_no_r, (xi, alpha, beta)), + vbar(sp.Symbol('i') - r + j) +) + +# ---- Step 5: 生成 LaTeX 输出(array 环境) ---- +def generate_latex_array(*exprs, alpha_expr, beta_expr): + lines = [] + for expr in exprs: + lines.append(latex(expr)) + + # 添加 α, β 定义(j 范围) + j_range = r'j=0,1,\ldots,k-1' + alpha_line = f"{latex(alpha_expr)}={latex(alpha)},\\quad {j_range}" + beta_line = f"{latex(beta_expr)}={latex(beta)},\\quad {j_range}" + + full_latex = r"\begin{array}{l}" + "\n" + full_latex += " \\\\\n".join(lines + [alpha_line, beta_line]) + full_latex += "\n\\end{array}" + return full_latex + +# ---- 执行生成 ---- +# 定义 α(r,j), β(r,j) 作为符号(用于等式左边) +alpha_sym = sp.Symbol(r'\alpha(r,j)') +beta_sym = sp.Symbol(r'\beta(r,j)') + +latex_output = generate_latex_array( + sp.Eq(sp.Symbol('p(r,\\xi)'), p_r_xi), + sp.Eq(sp.Integral(sp.Symbol('p(r,\\xi)'), (xi, alpha_sym, beta_sym)), vbar(sp.Symbol('i') - r + j)), + integral_eq_expanded, + integral_eq_no_r, + alpha_expr=alpha_sym, + beta_expr=beta_sym +) + +print(latex_output) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02b/matrix.py b/example/figure/1d/weno/matrix/02b/matrix.py new file mode 100644 index 00000000..4e90596e --- /dev/null +++ b/example/figure/1d/weno/matrix/02b/matrix.py @@ -0,0 +1,89 @@ +import sympy as sp +from sympy import symbols, Function, latex, Rational + +# ---------- 配置 ---------- +# 当前仅用于 LaTeX 显示,不用于求和 +k = sp.Symbol('k', integer=True, positive=True) +r, j, xi = symbols('r j \\xi') +i_sym = symbols('i') # 用于 vbar 下标 + +# 多项式系数函数(用于具体 k 时) +a = Function('a') + +# ---------- 工具函数 ---------- +def vbar_expr(): + """返回 \overline{v}_{i - r + j} 的 LaTeX 表示""" + index = i_sym - r + j + return r"\overline{v}_{" + latex(index) + r"}" + +def polynomial_latex_with_cdots(use_r=True): + """生成 a_0 + a_1 \\xi + \\cdots + a_{k-1} \\xi^{k-1} 的 LaTeX""" + if use_r: + term0 = r"a_{0}(r)" + term1 = r"a_{1}(r) \xi" + term_last = r"a_{k-1}(r) \xi^{k-1}" + else: + term0 = r"a_{0}" + term1 = r"a_{1} \xi" + term_last = r"a_{k-1} \xi^{k-1}" + return f"{term0} + {term1} + \\cdots + {term_last}" + +def integral_latex_with_cdots(use_r=True): + """生成带省略号的积分表达式 LaTeX""" + poly_str = polynomial_latex_with_cdots(use_r) + alpha_str = latex(-r + j - Rational(1, 2)) + beta_str = latex(-r + j + Rational(1, 2)) + vbar_str = vbar_expr() + return f"\\displaystyle \\int_{{{alpha_str}}}^{{{beta_str}}} ({poly_str}) \\, d\\xi = {vbar_str}" + +# ---------- 构造具体 k 的表达式(用于将来矩阵生成) ---------- +def build_full_polynomial(k_val, use_r=True): + """当 k 是具体整数时,构建完整多项式(用于计算)""" + terms = [] + for m in range(k_val): + if use_r: + coeff = a(m)(r) + else: + coeff = sp.Symbol(f'a_{m}') + terms.append(coeff * xi**m) + return sum(terms) + +# ---------- 生成最终 LaTeX array ---------- +def generate_weno_latex_array(): + lines = [] + + # Line 1: p(r,xi) = a0(r) + a1(r) xi + ... + a_{k-1}(r) xi^{k-1} + p_def = r"p(r,\xi) = " + polynomial_latex_with_cdots(use_r=True) + lines.append(p_def) + + # Line 2: ∫ p(r,xi) dxi = vbar + alpha_sym = r"\alpha(r,j)" + beta_sym = r"\beta(r,j)" + vbar_str = vbar_expr() + lines.append(rf"\displaystyle\int_{{{alpha_sym}}}^{{{beta_sym}}} p(r,\xi) \, d\xi = {vbar_str}") + + # Line 3: 展开积分(带 a_m(r)) + lines.append(integral_latex_with_cdots(use_r=True)) + + # Line 4: 展开积分(不带 r) + lines.append(integral_latex_with_cdots(use_r=False)) + + # Line 5-6: α, β 定义 + alpha_def = latex(-r + j - Rational(1, 2)) + beta_def = latex(-r + j + Rational(1, 2)) + j_range = r"j=0,1,\ldots,k-1" + lines.append(rf"\alpha(r,j)={alpha_def},\quad {j_range}") + lines.append(rf"\beta(r,j)={beta_def},\quad {j_range}") + + # 组合成 array + latex_output = "\\begin{array}{l}\n" + " \\\\\n".join(lines) + "\n\\end{array}" + return latex_output + +# ---------- 主程序 ---------- +if __name__ == "__main__": + latex_code = generate_weno_latex_array() + print(latex_code) + + # 未来:当你需要 k=3 的具体系统时 + # p3 = build_full_polynomial(3, use_r=True) + # print("p3 =", p3) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02c/matrix.py b/example/figure/1d/weno/matrix/02c/matrix.py new file mode 100644 index 00000000..fbe89518 --- /dev/null +++ b/example/figure/1d/weno/matrix/02c/matrix.py @@ -0,0 +1,123 @@ +import sympy as sp +from sympy import symbols, Function, Rational, latex, Matrix, Integral + +# ---------- 全局符号 ---------- +k = sp.Symbol('k', integer=True, positive=True) +r, j, xi = symbols('r j \\xi') +i_sym = symbols('i') + +# ---------- 辅助函数 ---------- +def vbar(idx): + """生成 \overline{v}_{i - r + idx}""" + return sp.Symbol(r"\overline{v}_{" + latex(i_sym - r + idx) + r"}") + +def alpha(j): return -r + j - Rational(1, 2) +def beta(j): return -r + j + Rational(1, 2) + +# ---------- 1. 生成通用矩阵 M 的 LaTeX(带 \cdots) ---------- +def matrix_M_latex_generic(): + """生成 M = [ ∫ ξ^m dξ ]_{j,m=0}^{k-1} 的通用 LaTeX(含省略号)""" + rows = [] + # 第一行:j=0 + first_row = " & ".join([ + r"\int_{\alpha_{" + "0" + r"}}^{\beta_{" + "0" + r"}} d\xi", + r"\int_{\alpha_{" + "0" + r"}}^{\beta_{" + "0" + r"}} \xi^{1} d\xi", + r"\cdots", + r"\int_{\alpha_{" + "0" + r"}}^{\beta_{" + "0" + r"}} \xi^{k-1} d\xi" + ]) + rows.append(first_row) + + # 第二行(示例 j=1) + second_row = " & ".join([ + r"\int_{\alpha_{" + "1" + r"}}^{\beta_{" + "1" + r"}} d\xi", + r"\int_{\alpha_{" + "1" + r"}}^{\beta_{" + "1" + r"}} \xi^{1} d\xi", + r"\cdots", + r"\int_{\alpha_{" + "1" + r"}}^{\beta_{" + "1" + r"}} \xi^{k-1} d\xi" + ]) + rows.append(second_row) + + # 省略行 + rows.append(r"\vdots & \vdots & \ddots & \vdots") + + # 最后一行:j = k-1 + last_idx = "k-1" + last_row = " & ".join([ + r"\int_{\alpha_{" + last_idx + r"}}^{\beta_{" + last_idx + r"}} d\xi", + r"\int_{\alpha_{" + last_idx + r"}}^{\beta_{" + last_idx + r"}} \xi^{1} d\xi", + r"\cdots", + r"\int_{\alpha_{" + last_idx + r"}}^{\beta_{" + last_idx + r"}} \xi^{k-1} d\xi" + ]) + rows.append(last_row) + + matrix_body = " \\\\\n".join(rows) + return r"M = \begin{bmatrix}" + "\n" + matrix_body + "\n\\end{bmatrix}" + +# ---------- 2. 生成具体 M 矩阵(用于计算和 LaTeX) ---------- +def build_matrix_M(k_val, r_val=None): + """ + 构建具体 k 下的 M 矩阵。 + 若 r_val 为 None,则保留 r 为符号;否则代入具体 r。 + """ + M = sp.zeros(k_val, k_val) + for j_idx in range(k_val): + a_j = alpha(j_idx) + b_j = beta(j_idx) + if r_val is not None: + a_j = a_j.subs(r, r_val) + b_j = b_j.subs(r, r_val) + for m in range(k_val): + # ∫ ξ^m dξ from α to β + integral = (b_j**(m+1) - a_j**(m+1)) / Rational(m+1) + M[j_idx, m] = sp.simplify(integral) + return M + +# ---------- 3. 生成向量 LaTeX ---------- +def vector_a_latex(): + elements = [f"a_{{{i}}}" for i in range(k-1)] # 仅用于显示 + # 但 k 是符号,无法 range(k),所以用省略号 + return r"\mathbf{a} = \begin{bmatrix} a_0 \\ a_1 \\ \\vdots \\ a_{k-1} \\end{bmatrix}" + +def vector_v_latex(): + # 使用 vbar(j) for j=0,...,k-1 + first = r"\overline{v}_{" + latex(i_sym - r + 0) + r"}" + second = r"\overline{v}_{" + latex(i_sym - r + 1) + r"}" + last = r"\overline{v}_{" + latex(i_sym - r + (k - 1)) + r"}" + return f"\\mathbf{{v}} = \\begin{bmatrix} {first} \\\\ {second} \\\\ \\vdots \\\\ {last} \\end{{bmatrix}}" + +# ---------- 4. 生成 a = M^{-1} v 的公式 ---------- +def equation_system_latex(): + a_vec = r"\begin{bmatrix} a_0 \\ a_1 \\ \\vdots \\ a_{k-1} \\end{bmatrix}" + v_vec = r"\begin{bmatrix} \overline{v}_{" + latex(i_sym - r + 0) + r"} \\ \overline{v}_{" + latex(i_sym - r + 1) + r"} \\ \\vdots \\ \overline{v}_{" + latex(i_sym - r + (k - 1)) + r"} \\end{bmatrix}" + return f"{a_vec} = M^{{-1}} {v_vec}" + +# ---------- 主输出:完整 LaTeX 块 ---------- +def generate_extended_latex(): + lines = [] + + # 向量定义 + lines.append(r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}") + lines.append(r"\mathbf{v}=[\overline{v}_{" + latex(i_sym - r + 0) + r"},\overline{v}_{" + latex(i_sym - r + 1) + r"},\dots,\overline{v}_{" + latex(i_sym - r + (k - 1)) + r"}]^T") + + # 方程 + lines.append(r"\mathbf{a}=M^{-1}\mathbf{v}") + + # 展开方程 + #a_vec = r"\begin{bmatrix} a_0 \\ a_1 \\ \\vdots \\ a_{k-1} \\end{bmatrix}" + a_vec = r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix}" + #v_vec = r"\begin{bmatrix} \overline{v}_{" + latex(i_sym - r + 0) + r"} \\ \overline{v}_{" + latex(i_sym - r + 1) + r"} \\ \\vdots \\ \overline{v}_{" + latex(i_sym - r + (k - 1)) + r"} \\end{bmatrix}" + v_vec = r"\begin{bmatrix} \overline{v}_{" + latex(i_sym - r + 0) + r"} \\ \overline{v}_{" + latex(i_sym - r + 1) + r"} \\ \vdots \\ \overline{v}_{" + latex(i_sym - r + (k - 1)) + r"} \end{bmatrix}" + lines.append(f"{a_vec} = M^{{-1}} {v_vec}") + + # 矩阵 M(通用形式) + lines.append(matrix_M_latex_generic()) + + return "\\begin{array}{l}\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- 示例:打印通用公式 ---------- +if __name__ == "__main__": + print(generate_extended_latex()) + + # 示例:打印 k=3, r=0 时的具体 M 矩阵 + print("\n\n=== Example: M for k=3, r=0 ===") + M3 = build_matrix_M(3, r_val=0) + print(latex(M3)) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02d/matrix.py b/example/figure/1d/weno/matrix/02d/matrix.py new file mode 100644 index 00000000..e0a7c286 --- /dev/null +++ b/example/figure/1d/weno/matrix/02d/matrix.py @@ -0,0 +1,91 @@ +import sympy as sp +from sympy import symbols, Rational, Matrix, latex + +# ---------- 全局符号(保持一致性) ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols('\\xi') + +# ---------- 核心函数 ---------- +def alpha(j): return -r + j - Rational(1, 2) +def beta(j): return -r + j + Rational(1, 2) + +def vbar(idx_expr): + """生成 \overline{v}_{expr} 的 SymPy 符号(用于计算)""" + # 使用带 LaTeX 名称的 Symbol,确保 latex() 输出正确 + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +def build_v_vector(k_val): + """构建 v = [v_{i-r}, v_{i-r+1}, ..., v_{i-r+k-1}]^T""" + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_is_symbolic=True, specific_r=None): + """ + 构建 M_{j,m} = ∫_{α_j}^{β_j} ξ^m dξ, j,m = 0,...,k-1 + + 参数: + k_val (int): 多项式阶数 k + r_is_symbolic (bool): 是否保留 r 为符号 + specific_r (int): 若 r_is_symbolic=False,指定具体 r 值 + """ + if not r_is_symbolic and specific_r is None: + raise ValueError("specific_r must be provided if r_is_symbolic=False") + + M = sp.zeros(k_val, k_val) + r_use = r if r_is_symbolic else specific_r + + for j in range(k_val): + a_j = (-r_use + j - Rational(1, 2)) + b_j = (-r_use + j + Rational(1, 2)) + for m in range(k_val): + # ∫ ξ^m dξ = (b^{m+1} - a^{m+1}) / (m+1) + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = sp.simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + """ + 求解 a = M^{-1} v + + 返回: + a_vec: 系数向量 [a0, a1, ..., a_{k-1}]^T (SymPy Matrix) + M: 使用的 M 矩阵 + v: 使用的 v 向量 + """ + if r_val is None: + # 保留 r 为符号(第 1 层) + M = build_M_matrix(k_val, r_is_symbolic=True) + v = build_v_vector(k_val) + else: + # 固定 r(第 2 层) + M = build_M_matrix(k_val, r_is_symbolic=False, specific_r=r_val) + # 构建 v 向量(此时 r 已固定) + v = Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + + # 求逆并计算 a = M^{-1} v + M_inv = M.inv() # SymPy 会自动简化 + a_vec = M_inv * v + return a_vec, M, v + +# ---------- 工具:美化 LaTeX 输出 ---------- +def print_solution_for_k_r(k_val, r_val=None): + """打印 a = M^{-1} v 的完整 LaTeX(通用或具体)""" + a_vec, M, v = solve_coefficients(k_val, r_val) + + print(f"\n=== WENO Coefficients: k={k_val}" + (f", r={r_val}" if r_val is not None else "(r symbolic)") + " ===") + print("M =") + print(latex(M)) + print("\nv =") + print(latex(v)) + print("\na = M^{-1} v =") + print(latex(a_vec)) + return a_vec, M, v + +# ---------- 主程序:按需生成 ---------- +if __name__ == "__main__": + # 示例 1: k=3, r 保持符号(通用表达式) + print_solution_for_k_r(k_val=3, r_val=None) + + # 示例 2: k=3, r=0,1,2(具体表达式) + for r_example in [0, 1, 2]: + print_solution_for_k_r(k_val=3, r_val=r_example) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02e/matrix.py b/example/figure/1d/weno/matrix/02e/matrix.py new file mode 100644 index 00000000..782c9438 --- /dev/null +++ b/example/figure/1d/weno/matrix/02e/matrix.py @@ -0,0 +1,219 @@ +import sympy as sp +import re +from sympy import symbols, Function, Rational, Matrix, latex, simplify + +# ---------- 全局符号(保持一致性) ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- 辅助函数:vbar 符号 ---------- +def vbar(idx_expr): + """生成带 LaTeX 名称的 \overline{v}_{...} 符号""" + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +# ---------- 积分限 ---------- +def alpha(j): return -r + j - Rational(1, 2) +def beta(j): return -r + j + Rational(1, 2) + +# ---------- 向量与矩阵构造 ---------- +def build_v_vector(k_val, r_val=None): + """构建 v = [v_{i - r + j}]_{j=0}^{k-1}""" + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + """构建 M_{j,m} = ∫_{α_j}^{β_j} ξ^m dξ""" + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is None: + a_j = alpha(j) + b_j = beta(j) + else: + a_j = (-r_val + j - Rational(1, 2)) + b_j = (-r_val + j + Rational(1, 2)) + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + """求解 a = M^{-1} v""" + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- LaTeX 输出:原始公式 ---------- +def generate_original_latex(): + """生成你最初要求的 LaTeX 公式块""" + k_sym = sp.Symbol('k') + lines = [ + r"p(r,\xi) = a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}", + r"\displaystyle\int_{\alpha(r,j)}^{\beta(r,j)} p(r,\xi) d\xi=\overline{v}_{i-r+j}", + r"\displaystyle \int_{\alpha(r,j)}^{\beta(r,j)} (a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}) d\xi=\overline{v}_{i-r+j}", + r"\displaystyle \int_{\alpha(r,j)}^{\beta(r,j)} (a_{0} + a_{1} \xi + a_{2} \xi^2 + \cdots + a_{k-1} \xi^{k-1}) d\xi=\overline{v}_{i-r+j}", + r"\alpha(r,j)=-r+j-1/2,\quad j=0,1,\cdots,k-1", + r"\beta(r,j)=-r+j+1/2,\quad j=0,1,\cdots,k-1" + ] + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- LaTeX 输出:扩展系统 ---------- +def generate_extended_latex(): + """生成 a = M^{-1} v 系统的 LaTeX""" + k_sym = sp.Symbol('k') + v0 = vbar(i_sym - r + 0) + v1 = vbar(i_sym - r + 1) + vk1 = vbar(i_sym - r + (k_sym - 1)) + + lines = [ + r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}", + r"\mathbf{v}=[\overline{v}_{i - r},\overline{v}_{i - r + 1},\dots,\overline{v}_{i + k - r - 1}]^T", + r"\mathbf{a}=M^{-1}\mathbf{v}", + r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix} = M^{-1} \begin{bmatrix} \overline{v}_{i - r} \\ \overline{v}_{i - r + 1} \\ \vdots \\ \overline{v}_{i + k - r - 1} \end{bmatrix}" + ] + + # 矩阵 M(通用形式) + M_latex = r"M = \begin{bmatrix}" + "\n" + M_latex += r"\int_{\alpha_{0}}^{\beta_{0}}d\xi & \int_{\alpha_{0}}^{\beta_{0}}{\xi}^{1}d\xi & \cdots & \int_{\alpha_{0}}^{\beta_{0}}{\xi}^{k-1}d\xi \\" + "\n" + M_latex += r"\int_{\alpha_{1}}^{\beta_{1}}d\xi & \int_{\alpha_{1}}^{\beta_{1}}{\xi}^{1}d\xi & \cdots & \int_{\alpha_{1}}^{\beta_{1}}{\xi}^{k-1}d\xi \\" + "\n" + M_latex += r"\vdots & \vdots & \ddots & \vdots \\" + "\n" + M_latex += r"\int_{\alpha_{k-1}}^{\beta_{k-1}}d\xi & \int_{\alpha_{k-1}}^{\beta_{k-1}}{\xi}^{1}d\xi & \cdots & \int_{\alpha_{k-1}}^{\beta_{k-1}}{\xi}^{k-1}d\xi" + "\n" + M_latex += r"\end{bmatrix}" + + lines.append(M_latex) + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- 格式化输出:排序 + 可选 factored ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + return simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except: + return index_latex + +def _sort_key_from_indexOlddddd(index_expr): + if isinstance(index_expr, str): + return (0, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (float(offset), "") + else: + return (0, str(index_expr)) + except: + return (0, str(index_expr)) + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) + else: + return (0.5, str(offset)) + except: + return (1, str(index_expr)) + +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + if not expr.has(_is_vbar_symbol): + return latex(expr) + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed.append((term, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Term has multiple vbar: {term}") + v = v_syms[0] + coeff = simplify(term / v) + idx = _extract_index_from_vbar(v) + parsed.append((coeff, v, idx)) + + # 排序:常数项在前(或最后),vbar 项按 index 排 + v_terms = [p for p in parsed if p[1] is not None] + const_terms = [p for p in parsed if p[1] is None] + v_terms.sort(key=lambda x: _sort_key_from_index(x[2])) + all_terms = const_terms + v_terms # 或 v_terms + const_terms + + parts = [] + for item in all_terms: + if item[1] is None: + parts.append(latex(item[0])) + else: + coeff, v, _ = item + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + num = "" if abs(coeff.p) == 1 else str(abs(coeff.p)) + den = str(coeff.q) + if num == "": + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = f"{sign}\\frac{{{num} {latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + parts.append(s) + + # 合并 + result = parts[0] + for part in parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +# ---------- 主输出函数 ---------- +def print_solution_for_k_r(k_val, r_val=None, factored=True): + a_vec, M, v = solve_coefficients(k_val, r_val) + desc = f"k={k_val}" + (f", r={r_val}" if r_val is not None else " (r symbolic)") + print(f"\n=== WENO Coefficients: {desc} ===") + print(latex_a_vector_formatted(a_vec, factored=factored)) + return a_vec, M, v + +# ---------- 主程序 ---------- +if __name__ == "__main__": + # 1. 输出原始 LaTeX 公式 + print("=== Original LaTeX Block ===") + print(generate_original_latex()) + + # 2. 输出扩展系统 LaTeX + print("\n\n=== Extended System LaTeX ===") + print(generate_extended_latex()) + + # 3. 计算并格式化输出系数(k=3, r=0,1,2) + for r_example in [0, 1, 2]: + print_solution_for_k_r(k_val=3, r_val=r_example, factored=True) + # 取消注释可查看另一种格式 + # print_solution_for_k_r(k_val=3, r_val=r_example, factored=False) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02f/matrix.py b/example/figure/1d/weno/matrix/02f/matrix.py new file mode 100644 index 00000000..4c2b7774 --- /dev/null +++ b/example/figure/1d/weno/matrix/02f/matrix.py @@ -0,0 +1,190 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- 全局符号 ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- 辅助函数 ---------- +def vbar(idx_expr): + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +def alpha(j): return -r + j - Rational(1, 2) +def beta(j): return -r + j + Rational(1, 2) + +# ---------- 向量与矩阵构造 ---------- +def build_v_vector(k_val, r_val=None): + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is None: + a_j = alpha(j) + b_j = beta(j) + else: + a_j = (-r_val + j - Rational(1, 2)) + b_j = (-r_val + j + Rational(1, 2)) + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- LaTeX: 原始公式 ---------- +def generate_original_latex(): + lines = [ + r"p(r,\xi) = a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}", + r"\displaystyle\int_{\alpha(r,j)}^{\beta(r,j)} p(r,\xi) d\xi=\overline{v}_{i-r+j}", + r"\displaystyle \int_{\alpha(r,j)}^{\beta(r,j)} (a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}) d\xi=\overline{v}_{i-r+j}", + r"\displaystyle \int_{\alpha(r,j)}^{\beta(r,j)} (a_{0} + a_{1} \xi + a_{2} \xi^2 + \cdots + a_{k-1} \xi^{k-1}) d\xi=\overline{v}_{i-r+j}", + r"\alpha(r,j)=-r+j-1/2,\quad j=0,1,\cdots,k-1", + r"\beta(r,j)=-r+j+1/2,\quad j=0,1,\cdots,k-1" + ] + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- LaTeX: 扩展系统 ---------- +def generate_extended_latex(): + lines = [ + r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}", + r"\mathbf{v}=[\overline{v}_{i - r},\overline{v}_{i - r + 1},\dots,\overline{v}_{i + k - r - 1}]^T", + r"\mathbf{a}=M^{-1}\mathbf{v}", + r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix} = M^{-1} \begin{bmatrix} \overline{v}_{i - r} \\ \overline{v}_{i - r + 1} \\ \vdots \\ \overline{v}_{i + k - r - 1} \end{bmatrix}" + ] + M_latex = r"M = \begin{bmatrix}" + "\n" + M_latex += r"\int_{\alpha_{0}}^{\beta_{0}}d\xi & \int_{\alpha_{0}}^{\beta_{0}}{\xi}^{1}d\xi & \cdots & \int_{\alpha_{0}}^{\beta_{0}}{\xi}^{k-1}d\xi \\" + "\n" + M_latex += r"\int_{\alpha_{1}}^{\beta_{1}}d\xi & \int_{\alpha_{1}}^{\beta_{1}}{\xi}^{1}d\xi & \cdots & \int_{\alpha_{1}}^{\beta_{1}}{\xi}^{k-1}d\xi \\" + "\n" + M_latex += r"\vdots & \vdots & \ddots & \vdots \\" + "\n" + M_latex += r"\int_{\alpha_{k-1}}^{\beta_{k-1}}d\xi & \int_{\alpha_{k-1}}^{\beta_{k-1}}{\xi}^{1}d\xi & \cdots & \int_{\alpha_{k-1}}^{\beta_{k-1}}{\xi}^{k-1}d\xi" + "\n" + M_latex += r"\end{bmatrix}" + lines.append(M_latex) + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- 排序与格式化 ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + return simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except: + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) + else: + return (0.5, str(offset)) + except: + return (1, str(index_expr)) + +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + + # 正确检测是否包含 vbar 符号 + has_vbar = any(_is_vbar_symbol(s) for s in expr.free_symbols) + if not has_vbar: + return latex(expr) + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Unexpected term: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + # 排序:vbar 项按 index 排序,常数项放前(WENO 中通常无常数项) + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) + return _sort_key_from_index(idx) + + parsed_terms.sort(key=term_sort_key) + + # 生成 LaTeX + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +# ---------- 主输出函数 ---------- +def print_solution_for_k_r(k_val, r_val=None, factored=True): + a_vec, M, v = solve_coefficients(k_val, r_val) + desc = f"k={k_val}" + (f", r={r_val}" if r_val is not None else " (r symbolic)") + print(f"\n=== WENO Coefficients: {desc} ===") + print(latex_a_vector_formatted(a_vec, factored=factored)) + return a_vec, M, v + +# ---------- 主程序 ---------- +if __name__ == "__main__": + # 1. 原始公式 + print("=== Original LaTeX Block ===") + print(generate_original_latex()) + + # 2. 扩展系统 + print("\n\n=== Extended System LaTeX ===") + print(generate_extended_latex()) + + # 3. 具体系数 (k=3, r=0,1,2) + for r_val in [0, 1, 2]: + print_solution_for_k_r(k_val=3, r_val=r_val, factored=True) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02g/matrix.py b/example/figure/1d/weno/matrix/02g/matrix.py new file mode 100644 index 00000000..2f69fcfa --- /dev/null +++ b/example/figure/1d/weno/matrix/02g/matrix.py @@ -0,0 +1,264 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- 全局符号 ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- 积分限命名策略 ---------- +# 提供基名(不含下标),如 \alpha, \beta +DEFAULT_BOUNDARY_NAMES = { + 'long_left': r'\alpha(r,j)', # 用于原始公式 + 'long_right': r'\beta(r,j)', + # M 矩阵中:\alpha_j 表示第 j 个单元左边界 + 'short_left_template': r'\alpha_{{{j}}}', # 用于 M 矩阵:\alpha_0, \alpha_1, ... + 'short_right_template': r'\beta_{{{j}}}' +} + +# ---------- 辅助函数 ---------- +def vbar(idx_expr): + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +def lower_limit(j): return -r + j - Rational(1, 2) +def upper_limit(j): return -r + j + Rational(1, 2) + +# ---------- 向量与矩阵构造 ---------- +def build_v_vector(k_val, r_val=None): + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is None: + a_j = lower_limit(j) + b_j = upper_limit(j) + else: + a_j = (-r_val + j - Rational(1, 2)) + b_j = (-r_val + j + Rational(1, 2)) + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- LaTeX: 原始公式 ---------- +def generate_original_latex(boundary_names=None): + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + left = boundary_names['long_left'] + right = boundary_names['long_right'] + lines = [ + r"p(r,\xi) = a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}", + rf"\displaystyle\int_{{{left}}}^{{{right}}} p(r,\xi) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}}(r)+a_{{1}}(r)\xi +\cdots+a_{{k-1}}(r)\xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}} + a_{{1}} \xi + \cdots + a_{{k-1}} \xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"{left}=-r+j-1/2,\quad j=0,1,\cdots,k-1", + rf"{right}=-r+j+1/2,\quad j=0,1,\cdots,k-1" + ] + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- LaTeX: 扩展系统 ---------- +def generate_extended_latexOld(boundary_names=None): + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + base_l = boundary_names['short_base_left'] # e.g., \alpha + base_r = boundary_names['short_base_right'] # e.g., \beta + + lines = [ + r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}", + r"\mathbf{v}=[\overline{v}_{i - r},\overline{v}_{i - r + 1},\dots,\overline{v}_{i + k - r - 1}]^T", + r"\mathbf{a}=M^{-1}\mathbf{v}", + r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix} = M^{-1} \begin{bmatrix} \overline{v}_{i - r} \\ \overline{v}_{i - r + 1} \\ \vdots \\ \overline{v}_{i + k - r - 1} \end{bmatrix}" + ] + + # 构建 M 矩阵 LaTeX:使用 \alpha_0, \alpha_1, \alpha_{k-1} + def make_row(j_str): + return " & ".join([ + rf"\int_{{{base_l}_{{{j_str}}}}}^{{{base_r}_{{{j_str}}}}} d\xi", + rf"\int_{{{base_l}_{{{j_str}}}}}^{{{base_r}_{{{j_str}}}}} \xi^{{1}} d\xi", + r"\cdots", + rf"\int_{{{base_l}_{{{j_str}}}}}^{{{base_r}_{{{j_str}}}}} \xi^{{k-1}} d\xi" + ]) + + M_body = " \\\\\n".join([ + make_row("0"), + make_row("1"), + r"\vdots & \vdots & \ddots & \vdots", + make_row("k-1") + ]) + M_latex = f"M = \\begin{{bmatrix}}\n{M_body}\n\\end{{bmatrix}}" + lines.append(M_latex) + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +def generate_extended_latex(boundary_names=None): + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + left_template = boundary_names['short_left_template'] # e.g., r'\alpha_{{{j}}}' + right_template = boundary_names['short_right_template'] # e.g., r'\beta_{{{j}}}' + + lines = [ + r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}", + r"\mathbf{v}=[\overline{v}_{i - r},\overline{v}_{i - r + 1},\dots,\overline{v}_{i + k - r - 1}]^T", + r"\mathbf{a}=M^{-1}\mathbf{v}", + r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix} = M^{-1} \begin{bmatrix} \overline{v}_{i - r} \\ \overline{v}_{i - r + 1} \\ \vdots \\ \overline{v}_{i + k - r - 1} \end{bmatrix}" + ] + + def make_row(j_str): + left = left_template.format(j=j_str) + right = right_template.format(j=j_str) + return " & ".join([ + rf"\int_{{{left}}}^{{{right}}} d\xi", + rf"\int_{{{left}}}^{{{right}}} \xi^{{1}} d\xi", + r"\cdots", + rf"\int_{{{left}}}^{{{right}}} \xi^{{k-1}} d\xi" + ]) + + M_body = " \\\\\n".join([ + make_row("0"), + make_row("1"), + r"\vdots & \vdots & \ddots & \vdots", + make_row("k-1") + ]) + M_latex = f"M = \\begin{{bmatrix}}\n{M_body}\n\\end{{bmatrix}}" + lines.append(M_latex) + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- 排序与格式化(无变动) ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + return simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except: + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) + else: + return (0.5, str(offset)) + except: + return (1, str(index_expr)) + +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + has_vbar = any(_is_vbar_symbol(s) for s in expr.free_symbols) + if not has_vbar: + return latex(expr) + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Unexpected term: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) + return _sort_key_from_index(idx) + + parsed_terms.sort(key=term_sort_key) + + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +# ---------- 主输出函数 ---------- +def print_solution_for_k_r(k_val, r_val=None, factored=True): + a_vec, M, v = solve_coefficients(k_val, r_val) + desc = f"k={k_val}" + (f", r={r_val}" if r_val is not None else " (r symbolic)") + print(f"\n=== WENO Coefficients: {desc} ===") + print(latex_a_vector_formatted(a_vec, factored=factored)) + return a_vec, M, v + +# ---------- 示例:不同命名策略 ---------- +X_BOUNDARY_NAMES = { + 'long_left': r'x_{j-\frac{1}{2}}', + 'long_right': r'x_{j+\frac{1}{2}}', + # 对 M 矩阵,提供 j 的占位符,如 {j} - 1/2 + 'short_left_template': r'x_{{{j}-\frac{{1}}{{2}}}}', + 'short_right_template': r'x_{{{j}+\frac{{1}}{{2}}}}' +} + +# ---------- 主程序 ---------- +if __name__ == "__main__": + # 1. 默认命名 (\alpha, \beta) + print("=== Original LaTeX (default: \\alpha, \\beta) ===") + print(generate_original_latex()) + + print("\n\n=== Extended System (default) ===") + print(generate_extended_latex()) + + # 2. 使用 x_{j±1/2} 命名 + print("\n\n=== Original LaTeX (x_{j±1/2}) ===") + print(generate_original_latex(boundary_names=X_BOUNDARY_NAMES)) + + print("\n\n=== Extended System (x_{j±1/2}) ===") + print(generate_extended_latex(boundary_names=X_BOUNDARY_NAMES)) + + # 3. 系数输出 (不受命名影响) + for r_val in [0, 1, 2]: + print_solution_for_k_r(k_val=3, r_val=r_val, factored=True) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02h/matrix.py b/example/figure/1d/weno/matrix/02h/matrix.py new file mode 100644 index 00000000..eff7f06a --- /dev/null +++ b/example/figure/1d/weno/matrix/02h/matrix.py @@ -0,0 +1,289 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- Global symbols ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- Boundary naming strategy ---------- +# Template-based naming to avoid double subscripts and allow flexible notation +DEFAULT_BOUNDARY_NAMES = { + 'long_left': r'\alpha(r,j)', # Used in original formulas + 'long_right': r'\beta(r,j)', + 'short_left_template': r'\alpha_{{{j}}}', # Template for M matrix: \alpha_{j} + 'short_right_template': r'\beta_{{{j}}}' +} + +# Alternative naming for cell-centered finite volume methods +X_BOUNDARY_NAMES = { + 'long_left': r'x_{j-\frac{1}{2}}', + 'long_right': r'x_{j+\frac{1}{2}}', + 'short_left_template': r'x_{{{j}-\frac{{1}}{{2}}}}', + 'short_right_template': r'x_{{{j}+\frac{{1}}{{2}}}}' +} + +# ---------- Helper functions ---------- +def vbar(idx_expr): + """Create a Symbol with LaTeX name \overline{v}_{idx_expr} for proper rendering.""" + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +def lower_limit(j): + """Lower integration limit: α(r,j) = -r + j - 1/2""" + return -r + j - Rational(1, 2) + +def upper_limit(j): + """Upper integration limit: β(r,j) = -r + j + 1/2""" + return -r + j + Rational(1, 2) + +# ---------- Vector and matrix construction ---------- +def build_v_vector(k_val, r_val=None): + """Construct the right-hand side vector v = [v̄_{i-r+j}]_{j=0}^{k-1}.""" + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + """ + Construct the moment matrix M where M[j,m] = ∫_{α_j}^{β_j} ξ^m dξ. + + Parameters: + k_val (int): Polynomial order (degree k-1) + r_val (int, optional): Specific r value. If None, keep r symbolic. + """ + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is not None: + # Substitute specific r value into symbolic limits + a_j = lower_limit(j).subs(r, r_val) + b_j = upper_limit(j).subs(r, r_val) + else: + a_j = lower_limit(j) + b_j = upper_limit(j) + + for m in range(k_val): + # Analytical integration: ∫ ξ^m dξ = (β^{m+1} - α^{m+1}) / (m+1) + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + """Solve a = M^{-1} v for polynomial coefficients.""" + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- LaTeX generation: Original formulas ---------- +def generate_original_latex(boundary_names=None): + """ + Generate the original LaTeX block with integration constraints. + + Uses boundary_names['long_left'] and boundary_names['long_right'] + for integration limits. + """ + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + left = boundary_names['long_left'] + right = boundary_names['long_right'] + + lines = [ + r"p(r,\xi) = a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}", + rf"\displaystyle\int_{{{left}}}^{{{right}}} p(r,\xi) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}}(r)+a_{{1}}(r)\xi +\cdots+a_{{k-1}}(r)\xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}} + a_{{1}} \xi + \cdots + a_{{k-1}} \xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"{left}=-r+j-1/2,\quad j=0,1,\cdots,k-1", + rf"{right}=-r+j+1/2,\quad j=0,1,\cdots,k-1" + ] + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- LaTeX generation: Extended system ---------- +def generate_extended_latex(boundary_names=None): + """ + Generate the extended system LaTeX including a = M^{-1}v and matrix M. + + Uses template-based naming for M matrix entries to avoid double subscripts. + Templates should contain {j} placeholder for row index. + """ + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + + left_template = boundary_names['short_left_template'] + right_template = boundary_names['short_right_template'] + + lines = [ + r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}", + r"\mathbf{v}=[\overline{v}_{i - r},\overline{v}_{i - r + 1},\dots,\overline{v}_{i + k - r - 1}]^T", + r"\mathbf{a}=M^{-1}\mathbf{v}", + r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix} = M^{-1} \begin{bmatrix} \overline{v}_{i - r} \\ \overline{v}_{i - r + 1} \\ \vdots \\ \overline{v}_{i + k - r - 1} \end{bmatrix}" + ] + + def make_row(j_str): + """Generate a matrix row with proper boundary notation.""" + # Safe template substitution + left = left_template.format(j=j_str) + right = right_template.format(j=j_str) + return " & ".join([ + rf"\int_{{{left}}}^{{{right}}} d\xi", + rf"\int_{{{left}}}^{{{right}}} \xi^{{1}} d\xi", + r"\cdots", + rf"\int_{{{left}}}^{{{right}}} \xi^{{k-1}} d\xi" + ]) + + # Construct matrix with representative rows (0, 1, ..., k-1) + M_body = " \\\\\n".join([ + make_row("0"), + make_row("1"), + r"\vdots & \vdots & \ddots & \vdots", + make_row("k-1") + ]) + M_latex = f"M = \\begin{{bmatrix}}\n{M_body}\n\\end{{bmatrix}}" + lines.append(M_latex) + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- Expression formatting with sorted vbar terms ---------- +def _is_vbar_symbol(sym): + """Check if symbol represents \overline{v}_{...}.""" + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + """Extract the index expression from \overline{v}_{index} symbol name.""" + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + # Evaluate in context of global symbols i and r + return simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except: + return index_latex + +def _sort_key_from_index(index_expr): + """ + Generate sort key for vbar indices. + Priority: numeric offsets from i (i-1, i, i+1, ...) > symbolic > strings. + """ + if isinstance(index_expr, str): + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) # Sort by numeric offset + else: + return (0.5, str(offset)) # Symbolic offsets + except: + return (1, str(index_expr)) + +def format_a_expression(expr, factored=True): + """ + Format polynomial coefficient expression with vbar terms sorted by index. + + Parameters: + expr: SymPy expression for a coefficient + factored: If True, output as (1/24)*v̄; if False, output as v̄/24 + """ + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + + # Check if expression contains any vbar symbols + has_vbar = any(_is_vbar_symbol(s) for s in expr.free_symbols) + if not has_vbar: + return latex(expr) + + # Parse terms into (coefficient, vbar_symbol, index) tuples + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Unexpected term: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + # Sort terms: vbar terms by index offset, constants first + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) # Constants first + return _sort_key_from_index(idx) + + parsed_terms.sort(key=term_sort_key) + + # Generate LaTeX for each term + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + # Add parentheses for compound coefficients + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + # Fraction format: v̄/denominator + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + # Combine terms with proper sign handling + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + """Format coefficient vector as bmatrix with sorted terms.""" + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +# ---------- Main output functions ---------- +def print_solution_for_k_r(k_val, r_val=None, factored=True): + """Print formatted coefficient vector for given k and r values.""" + a_vec, M, v = solve_coefficients(k_val, r_val) + desc = f"k={k_val}" + (f", r={r_val}" if r_val is not None else " (r symbolic)") + print(f"\n=== WENO Coefficients: {desc} ===") + print(latex_a_vector_formatted(a_vec, factored=factored)) + return a_vec, M, v + +# ---------- Main execution ---------- +if __name__ == "__main__": + # 1. Default naming (\alpha, \beta) + print("=== Original LaTeX (default: \\alpha, \\beta) ===") + print(generate_original_latex()) + + print("\n\n=== Extended System (default) ===") + print(generate_extended_latex()) + + # 2. Cell-centered naming (x_{j±1/2}) + print("\n\n=== Original LaTeX (x_{j±1/2}) ===") + print(generate_original_latex(boundary_names=X_BOUNDARY_NAMES)) + + print("\n\n=== Extended System (x_{j±1/2}) ===") + print(generate_extended_latex(boundary_names=X_BOUNDARY_NAMES)) + + # 3. Coefficient solutions for k=3, r=0,1,2 + for r_val in [0, 1, 2]: + print_solution_for_k_r(k_val=3, r_val=r_val, factored=True) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/03/matrix.py b/example/figure/1d/weno/matrix/03/matrix.py new file mode 100644 index 00000000..05e388bb --- /dev/null +++ b/example/figure/1d/weno/matrix/03/matrix.py @@ -0,0 +1,352 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- Global symbols ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- Boundary naming strategy ---------- +DEFAULT_BOUNDARY_NAMES = { + 'long_left': r'\alpha(r,j)', + 'long_right': r'\beta(r,j)', + 'short_left_template': r'\alpha_{{{j}}}', + 'short_right_template': r'\beta_{{{j}}}' +} + +X_BOUNDARY_NAMES = { + 'long_left': r'x_{j-\frac{1}{2}}', + 'long_right': r'x_{j+\frac{1}{2}}', + 'short_left_template': r'x_{{{j}-\frac{{1}}{{2}}}}', + 'short_right_template': r'x_{{{j}+\frac{{1}}{{2}}}}' +} + +# ---------- Helper functions ---------- +def vbar(idx_expr): + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +def lower_limit(j): + return -r + j - Rational(1, 2) + +def upper_limit(j): + return -r + j + Rational(1, 2) + +# ---------- Vector and matrix construction ---------- +def build_v_vector(k_val, r_val=None): + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is not None: + a_j = lower_limit(j).subs(r, r_val) + b_j = upper_limit(j).subs(r, r_val) + else: + a_j = lower_limit(j) + b_j = upper_limit(j) + + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- NEW: Smoothness indicator Sr computation ---------- +def compute_Sr(k_val, r_val=None): + """ + Compute the WENO smoothness indicator Sr = sum_{l=1}^{k-1} ∫_{-1/2}^{1/2} (d^l p/dξ^l)^2 dξ + + Parameters: + k_val (int): Polynomial order (degree k-1) + r_val (int, optional): Specific r value. If None, keep r symbolic. + + Returns: + Sr_expr: Symbolic expression for Sr in terms of vbar values + p_expr: The polynomial p(r,ξ) + a_vec: Coefficient vector [a0, a1, ..., a_{k-1}] + """ + # Step 1: Get coefficients a = [a0, a1, ..., a_{k-1}] + a_vec, M, v = solve_coefficients(k_val, r_val) + + # Step 2: Construct polynomial p(r,ξ) = sum_{m=0}^{k-1} a_m * ξ^m + p_expr = sum(a_vec[m] * xi**m for m in range(k_val)) + + # Step 3: Compute Sr = sum_{l=1}^{k-1} ∫_{-1/2}^{1/2} (p^{(l)}(ξ))^2 dξ + Sr_expr = 0 + for l in range(1, k_val): # l = 1, 2, ..., k-1 + # Compute l-th derivative + p_l_deriv = sp.diff(p_expr, xi, l) + # Square it + p_l_squared = p_l_deriv**2 + # Integrate over [-1/2, 1/2] + integral_l = sp.integrate(p_l_squared, (xi, -Rational(1, 2), Rational(1, 2))) + Sr_expr += simplify(integral_l) + + # Final simplification + Sr_expr = simplify(Sr_expr) + return Sr_expr, p_expr, a_vec + +def format_Sr_expression(Sr_expr, factored=True): + """ + Format Sr expression with vbar terms sorted by index (similar to coefficient formatting). + """ + if Sr_expr.is_Number: + return latex(Sr_expr) + Sr_expr = simplify(Sr_expr) + + # Reuse the existing vbar detection and sorting logic + has_vbar = any(_is_vbar_symbol(s) for s in Sr_expr.free_symbols) + if not has_vbar: + return latex(Sr_expr) + + terms = Sr_expr.as_ordered_terms() if Sr_expr.is_Add else [Sr_expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + # Sr may have products of vbar terms (like v_i * v_{i+1}) + # For now, handle single vbar terms (which is the case for k=3) + # For higher k, we might have cross terms + if len(v_syms) == 1: + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + else: + # Handle cross terms or complex expressions + parsed_terms.append((term, None, None)) + + # Sort single vbar terms, keep others as-is + single_vbar_terms = [] + other_terms = [] + for item in parsed_terms: + if item[1] is not None: + single_vbar_terms.append(item) + else: + other_terms.append(item) + + # Sort single vbar terms + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) + return _sort_key_from_index(idx) + + single_vbar_terms.sort(key=term_sort_key) + all_terms = other_terms + single_vbar_terms + + # Generate LaTeX (simplified for Sr - usually no factoring needed) + latex_parts = [] + for coeff, v, _ in all_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + # For Sr, we typically want expanded form + latex_parts.append(latex(coeff * v)) + + # Combine terms + if not latex_parts: + return "0" + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +# ---------- Existing functions (unchanged) ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + return simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except: + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) + else: + return (0.5, str(offset)) + except: + return (1, str(index_expr)) + +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + has_vbar = any(_is_vbar_symbol(s) for s in expr.free_symbols) + if not has_vbar: + return latex(expr) + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Unexpected term: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) + return _sort_key_from_index(idx) + + parsed_terms.sort(key=term_sort_key) + + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +def generate_original_latex(boundary_names=None): + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + left = boundary_names['long_left'] + right = boundary_names['long_right'] + lines = [ + r"p(r,\xi) = a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}", + rf"\displaystyle\int_{{{left}}}^{{{right}}} p(r,\xi) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}}(r)+a_{{1}}(r)\xi +\cdots+a_{{k-1}}(r)\xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}} + a_{{1}} \xi + \cdots + a_{{k-1}} \xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"{left}=-r+j-1/2,\quad j=0,1,\cdots,k-1", + rf"{right}=-r+j+1/2,\quad j=0,1,\cdots,k-1" + ] + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +def generate_extended_latex(boundary_names=None): + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + left_template = boundary_names['short_left_template'] + right_template = boundary_names['short_right_template'] + + lines = [ + r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}", + r"\mathbf{v}=[\overline{v}_{i - r},\overline{v}_{i - r + 1},\dots,\overline{v}_{i + k - r - 1}]^T", + r"\mathbf{a}=M^{-1}\mathbf{v}", + r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix} = M^{-1} \begin{bmatrix} \overline{v}_{i - r} \\ \overline{v}_{i - r + 1} \\ \vdots \\ \overline{v}_{i + k - r - 1} \end{bmatrix}" + ] + + def make_row(j_str): + left = left_template.format(j=j_str) + right = right_template.format(j=j_str) + return " & ".join([ + rf"\int_{{{left}}}^{{{right}}} d\xi", + rf"\int_{{{left}}}^{{{right}}} \xi^{{1}} d\xi", + r"\cdots", + rf"\int_{{{left}}}^{{{right}}} \xi^{{k-1}} d\xi" + ]) + + M_body = " \\\\\n".join([ + make_row("0"), + make_row("1"), + r"\vdots & \vdots & \ddots & \vdots", + make_row("k-1") + ]) + M_latex = f"M = \\begin{{bmatrix}}\n{M_body}\n\\end{{bmatrix}}" + lines.append(M_latex) + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +def print_solution_for_k_r(k_val, r_val=None, factored=True): + a_vec, M, v = solve_coefficients(k_val, r_val) + desc = f"k={k_val}" + (f", r={r_val}" if r_val is not None else " (r symbolic)") + print(f"\n=== WENO Coefficients: {desc} ===") + print(latex_a_vector_formatted(a_vec, factored=factored)) + return a_vec, M, v + +# ---------- Main execution with Sr computation ---------- +if __name__ == "__main__": + k_val = 3 + + # Print original and extended systems + print("=== Original LaTeX (default) ===") + print(generate_original_latex()) + + print("\n\n=== Extended System (default) ===") + print(generate_extended_latex()) + + # Compute coefficients and Sr for r = 0, 1, 2 + for r_val in [0, 1, 2]: + print(f"\n" + "="*60) + print(f"=== k={k_val}, r={r_val} ===") + + # Coefficients + a_vec, M, v = solve_coefficients(k_val, r_val) + print("\nCoefficients a = M^{-1} v:") + print(latex_a_vector_formatted(a_vec, factored=True)) + + # Smoothness indicator Sr + Sr_expr, p_expr, a_vec_sr = compute_Sr(k_val, r_val) + print(f"\nSmoothness indicator S_{r_val}:") + print(format_Sr_expression(Sr_expr)) + + # Optional: Show the polynomial + print(f"\nPolynomial p({r_val},ξ):") + print(latex(p_expr)) + + # Also show symbolic Sr (r as symbol) for completeness + print(f"\n" + "="*60) + print(f"=== Symbolic Sr (k={k_val}, r symbolic) ===") + Sr_sym, p_sym, a_sym = compute_Sr(k_val, r_val=None) + print("S_r =") + print(format_Sr_expression(Sr_sym)) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/03a/matrix.py b/example/figure/1d/weno/matrix/03a/matrix.py new file mode 100644 index 00000000..ef2b909b --- /dev/null +++ b/example/figure/1d/weno/matrix/03a/matrix.py @@ -0,0 +1,254 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- Global symbols ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- Boundary naming strategy ---------- +DEFAULT_BOUNDARY_NAMES = { + 'long_left': r'\alpha(r,j)', + 'long_right': r'\beta(r,j)', + 'short_left_template': r'\alpha_{{{j}}}', + 'short_right_template': r'\beta_{{{j}}}' +} + +# ---------- Helper functions ---------- +def vbar(idx_expr): + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +def lower_limit(j): + return -r + j - Rational(1, 2) + +def upper_limit(j): + return -r + j + Rational(1, 2) + +# ---------- Vector and matrix construction ---------- +def build_v_vector(k_val, r_val=None): + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is not None: + a_j = lower_limit(j).subs(r, r_val) + b_j = upper_limit(j).subs(r, r_val) + else: + a_j = lower_limit(j) + b_j = upper_limit(j) + + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- Enhanced: Smoothness indicator Sr computation ---------- +def compute_Sr(k_val, r_val=None): + """ + Compute the WENO smoothness indicator Sr = sum_{l=1}^{k-1} ∫_{-1/2}^{1/2} (d^l p/dξ^l)^2 dξ + + This works for any k >= 2 and handles cross terms correctly. + """ + # Get coefficients a = [a0, a1, ..., a_{k-1}] + a_vec, M, v = solve_coefficients(k_val, r_val) + + # Construct polynomial p(r,ξ) = sum_{m=0}^{k-1} a_m * ξ^m + p_expr = sum(a_vec[m] * xi**m for m in range(k_val)) + + # Compute Sr = sum_{l=1}^{k-1} ∫_{-1/2}^{1/2} (p^{(l)}(ξ))^2 dξ + Sr_expr = 0 + for l in range(1, k_val): # l = 1, 2, ..., k-1 + p_l_deriv = sp.diff(p_expr, xi, l) + p_l_squared = p_l_deriv**2 + integral_l = sp.integrate(p_l_squared, (xi, -Rational(1, 2), Rational(1, 2))) + Sr_expr += simplify(integral_l) + + Sr_expr = simplify(Sr_expr) + return Sr_expr, p_expr, a_vec + +def format_Sr_expression(Sr_expr): + """ + Format Sr expression properly, handling both single terms and cross terms. + + For WENO smoothness indicators, the standard format is expanded form + showing all quadratic terms explicitly. + """ + if Sr_expr.is_Number: + return latex(Sr_expr) + + Sr_expr = simplify(Sr_expr) + + # For Sr, we want to expand everything to show the quadratic form + # This is the standard way smoothness indicators are presented in literature + expanded_expr = sp.expand(Sr_expr) + + # If it's just a number after expansion + if expanded_expr.is_Number: + return latex(expanded_expr) + + # Handle the general case by converting directly to LaTeX + # The expanded form will show all cross terms properly + return latex(expanded_expr) + +# ---------- Utility function to get vbar symbols from expression ---------- +def get_vbar_symbols(expr): + """Extract all vbar symbols from an expression.""" + return [s for s in expr.free_symbols if _is_vbar_symbol(s)] + +# ---------- Existing helper functions ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + return simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except: + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) + else: + return (0.5, str(offset)) + except: + return (1, str(index_expr)) + +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + has_vbar = any(_is_vbar_symbol(s) for s in expr.free_symbols) + if not has_vbar: + return latex(expr) + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Unexpected term: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) + return _sort_key_from_index(idx) + + parsed_terms.sort(key=term_sort_key) + + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +def generate_original_latex(boundary_names=None): + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + left = boundary_names['long_left'] + right = boundary_names['long_right'] + lines = [ + r"p(r,\xi) = a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}", + rf"\displaystyle\int_{{{left}}}^{{{right}}} p(r,\xi) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}}(r)+a_{{1}}(r)\xi +\cdots+a_{{k-1}}(r)\xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}} + a_{{1}} \xi + \cdots + a_{{k-1}} \xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"{left}=-r+j-1/2,\quad j=0,1,\cdots,k-1", + rf"{right}=-r+j+1/2,\quad j=0,1,\cdots,k-1" + ] + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- Main execution for both k=3 and k=4 ---------- +if __name__ == "__main__": + # Test both k=3 and k=4 + for k_val in [3, 4]: + print(f"\n{'='*80}") + print(f"COMPUTING FOR k = {k_val}") + print(f"{'='*80}") + + print("\n=== Original LaTeX ===") + print(generate_original_latex()) + + # Compute for r = 0, 1, ..., k-1 + for r_val in range(k_val): + print(f"\n" + "-"*60) + print(f"r = {r_val} (k = {k_val})") + + # Coefficients + a_vec, M, v = solve_coefficients(k_val, r_val) + print("\nCoefficients:") + print(latex_a_vector_formatted(a_vec, factored=True)) + + # Smoothness indicator + Sr_expr, p_expr, _ = compute_Sr(k_val, r_val) + print(f"\nSmoothness indicator S_{r_val}:") + print(format_Sr_expression(Sr_expr)) + + # Show the stencil size + v_symbols = get_vbar_symbols(Sr_expr) + if v_symbols: + print(f"\nStencil involves {len(v_symbols)} points: {[s.name for s in v_symbols]}") + + # Show symbolic version for k=4 + print(f"\n{'='*80}") + print("SYMBOLIC S_r FOR k=4 (r as symbol)") + print(f"{'='*80}") + Sr_sym, p_sym, _ = compute_Sr(4, r_val=None) + print("S_r =") + print(format_Sr_expression(Sr_sym)) \ No newline at end of file diff --git a/example/figure/1d/weno/smoothness/01/polynomial_operations.py b/example/figure/1d/weno/smoothness/01/polynomial_operations.py new file mode 100644 index 00000000..8b8799f9 --- /dev/null +++ b/example/figure/1d/weno/smoothness/01/polynomial_operations.py @@ -0,0 +1,913 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + #print(f"sum_integrals_same_bounds polynomials={polynomials}") + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + #print(f" Integration Result for Term{idx+1}: {format_expression(integral_result)}") + print(f" Integration Result for Term{idx+1}: {format_expression_fraction(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def format_expression_fraction(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + max_denominator = 1000000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + if len(symbols) == 1: + term_strs.append(f"{frac_str}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{frac_str}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{frac_str}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\nInitial Polynomial Expressions (before integration):") + for i, poly in enumerate(squared_polynomials, 1): + print(f" Polynomial Term {i}: P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def to_fraction(num, max_denominator=1000000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + return frac + +def float_to_fraction_str(num, max_denominator=1000000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}·" + frac_star = f"{frac_str}" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f"k={k},polynomial_list={polynomial_list}") + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + lower_bound = -0.5 + upper_bound = 0.5 + domain = f"[{to_fraction(lower_bound)}, {to_fraction(upper_bound)}]" + print(f"\nStep-by-step Integration and Summation (integration domain: x∈{domain}):") + + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + #print(f"k={k},total_result={total_result}") + + # 打印最终结果 + print(f"\nFinal Aggregated Result (sum of all integrated terms):") + #formatted_result = format_expression(total_result) + formatted_result = format_expression_fraction(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def sort_by_first_list(primary_list, *other_lists, key=None, reverse=False): + """ + 根据第一个列表排序,同步调整任意数量其他列表 + + 参数: + primary_list: 主排序参考列表 + *other_lists: 其他需要同步排序的列表(可变参数) + key: 排序key函数(如abs, lambda x: x**2等) + reverse: 是否降序 + + 返回: + 元组: (sorted_primary, sorted_other1, sorted_other2, ...) + """ + # 核心:动态生成key函数 + if key is None: + key_func = lambda i: primary_list[i] # 默认:直接比较值 + else: + key_func = lambda i: key(primary_list[i]) # 自定义:对值应用key函数 + + # 获取排序索引 + indices = sorted(range(len(primary_list)), key=key_func, reverse=reverse) + + # 应用索引到所有列表(包括主列表) + all_lists = (primary_list,) + other_lists + result = tuple([lst[i] for i in indices] for lst in all_lists) + return result + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + frac_list = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000000) + frac_list.append(frac) + #term_strs.append(f"{frac}·{symbol_str_final}") + term_strs.append(f"{frac}{symbol_str_final}") + + _, term_strs = sort_by_first_list(frac_list, term_strs, key=abs, reverse=True) + + return " + ".join(term_strs) + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def print_smoothness_indicators(expression,coeffs_list,k): + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + for r in range(k): + a_coeffs = coeffs_list[r] + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicatorOld(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + #print(f"k={k},a_coeffs={a_coeffs}") + coeffs_list.append( a_coeffs ) + + print_smoothness_indicators(total_result,coeffs_list,k) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + #demo_smoothness_indicator(1) + #demo_smoothness_indicator(2) + #demo_smoothness_indicator(3) + demo_smoothness_indicator(4) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/01/test_sorting.py b/example/figure/1d/weno/some_help_code/01/test_sorting.py new file mode 100644 index 00000000..a5a531fd --- /dev/null +++ b/example/figure/1d/weno/some_help_code/01/test_sorting.py @@ -0,0 +1,55 @@ +import sympy as sp +import re + +i_sym, r = sp.symbols('i r', integer=True) + +def _extract_index_from_vbar_name(name): + match = re.search(r'\\overline\{v\}_\{(.+)\}', name) + if not match: + return name + index_latex = match.group(1) + try: + # 注意:这里必须传入所有可能符号! + return sp.simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except Exception as e: + print(f"Eval failed for '{index_latex}': {e}") + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + return (1, index_expr) # 字符串放后面 + try: + offset = sp.simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) + else: + return (1, str(index_expr)) + except: + return (1, str(index_expr)) + +# 测试用例 +test_names = [ + r"\overline{v}_{i - 2}", + r"\overline{v}_{i + 1}", + r"\overline{v}_{i}", + r"\overline{v}_{i - 1}", + r"\overline{v}_{i + 2}", +] + +print("Test 1: Pure i + const") +indices = [_extract_index_from_vbar_name(name) for name in test_names] +print("Indices:", indices) +sorted_names = [name for _, name in sorted(zip(indices, test_names), key=lambda x: _sort_key_from_index(x[0]))] +print("Sorted:", sorted_names) + +# Test with r substituted (like r=0) +print("\nTest 2: After r=0 substitution (should be i, i+1, i+2)") +names_r0 = [ + r"\overline{v}_{i}", # j=0 + r"\overline{v}_{i + 1}", # j=1 + r"\overline{v}_{i + 2}", # j=2 +] +indices2 = [_extract_index_from_vbar_name(name) for name in names_r0] +print("Indices:", indices2) +sorted_names2 = [name for _, name in sorted(zip(indices2, names_r0), key=lambda x: _sort_key_from_index(x[0]))] +print("Sorted:", sorted_names2) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/01a/weno_debug_sort.py b/example/figure/1d/weno/some_help_code/01a/weno_debug_sort.py new file mode 100644 index 00000000..d5b5b18b --- /dev/null +++ b/example/figure/1d/weno/some_help_code/01a/weno_debug_sort.py @@ -0,0 +1,170 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- 全局符号 ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- vbar 构造 ---------- +def vbar(idx_expr): + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +# ---------- 积分限 ---------- +def alpha(j): return -r + j - Rational(1, 2) +def beta(j): return -r + j + Rational(1, 2) + +# ---------- 矩阵与向量 ---------- +def build_v_vector(k_val, r_val=None): + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is None: + a_j = alpha(j) + b_j = beta(j) + else: + a_j = (-r_val + j - Rational(1, 2)) + b_j = (-r_val + j + Rational(1, 2)) + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- 排序辅助函数 ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + # 使用全局 i_sym, r + index_expr = eval(index_latex, {'i': i_sym, 'r': r}) + return simplify(index_expr) + except Exception as e: + print(f"[EXTRACT FAILED] name={v_symbol.name}, index_latex={index_latex}, error={e}") + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + print(f" [SORT KEY] string fallback: {index_expr}") + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + key = (0, float(offset)) + print(f" [SORT KEY] {index_expr} → offset={offset} → key={key}") + return key + else: + key = (0.5, str(offset)) + print(f" [SORT KEY] non-numeric offset: {offset} → key={key}") + return key + except Exception as e: + key = (1, str(index_expr)) + print(f" [SORT KEY] exception: {e} → key={key}") + return key + +# ---------- 格式化表达式(带详细调试) ---------- +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + if not expr.has(_is_vbar_symbol): + return latex(expr) + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Term has multiple vbar: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + # === DEBUG: 打印解析结果 === + print(f"\n[DEBUG] Expression: {expr}") + for coeff, v, idx in parsed_terms: + if v is None: + print(f" CONSTANT: {coeff}") + else: + print(f" TERM: coeff={coeff}, v={v.name}, index_expr={idx} (type={type(idx)})") + + # === 排序 === + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) # 常数项最先 + return _sort_key_from_index(idx) + + print("[DEBUG] Sorting terms...") + parsed_terms.sort(key=term_sort_key) + + # === 生成 LaTeX === + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + # 合并 + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +# ---------- 主程序 ---------- +if __name__ == "__main__": + print("=== Testing k=3, r=0 ===") + a_vec, M, v = solve_coefficients(k_val=3, r_val=0) + print("\nRaw a_vec from SymPy:") + for i, ai in enumerate(a_vec): + print(f"a{i} = {ai}") + + print("\nFormatted output (factored=True):") + formatted = latex_a_vector_formatted(a_vec, factored=True) + print(formatted) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/01b/weno_debug_sort.py b/example/figure/1d/weno/some_help_code/01b/weno_debug_sort.py new file mode 100644 index 00000000..b168820c --- /dev/null +++ b/example/figure/1d/weno/some_help_code/01b/weno_debug_sort.py @@ -0,0 +1,173 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- 全局符号 ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- vbar 构造 ---------- +def vbar(idx_expr): + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +# ---------- 积分限 ---------- +def alpha(j): return -r + j - Rational(1, 2) +def beta(j): return -r + j + Rational(1, 2) + +# ---------- 矩阵与向量 ---------- +def build_v_vector(k_val, r_val=None): + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is None: + a_j = alpha(j) + b_j = beta(j) + else: + a_j = (-r_val + j - Rational(1, 2)) + b_j = (-r_val + j + Rational(1, 2)) + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- 排序辅助函数 ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + # 使用全局 i_sym, r + index_expr = eval(index_latex, {'i': i_sym, 'r': r}) + return simplify(index_expr) + except Exception as e: + print(f"[EXTRACT FAILED] name={v_symbol.name}, index_latex={index_latex}, error={e}") + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + print(f" [SORT KEY] string fallback: {index_expr}") + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + key = (0, float(offset)) + print(f" [SORT KEY] {index_expr} → offset={offset} → key={key}") + return key + else: + key = (0.5, str(offset)) + print(f" [SORT KEY] non-numeric offset: {offset} → key={key}") + return key + except Exception as e: + key = (1, str(index_expr)) + print(f" [SORT KEY] exception: {e} → key={key}") + return key + +# ---------- 格式化表达式(带详细调试) ---------- +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + + # === FIX: 正确检测 vbar 符号 === + has_vbar = any(_is_vbar_symbol(s) for s in expr.free_symbols) + if not has_vbar: + return latex(expr) + # ============================== + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Term has multiple vbar: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + # === DEBUG: 打印解析结果 === + print(f"\n[DEBUG] Expression: {expr}") + for coeff, v, idx in parsed_terms: + if v is None: + print(f" CONSTANT: {coeff}") + else: + print(f" TERM: coeff={coeff}, v={v.name}, index_expr={idx} (type={type(idx)})") + + # === 排序 === + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) + return _sort_key_from_index(idx) + + print("[DEBUG] Sorting terms...") + parsed_terms.sort(key=term_sort_key) + + # === 生成 LaTeX === + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +# ---------- 主程序 ---------- +if __name__ == "__main__": + print("=== Testing k=3, r=0 ===") + a_vec, M, v = solve_coefficients(k_val=3, r_val=0) + print("\nRaw a_vec from SymPy:") + for i, ai in enumerate(a_vec): + print(f"a{i} = {ai}") + + print("\nFormatted output (factored=True):") + formatted = latex_a_vector_formatted(a_vec, factored=True) + print(formatted) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02/testprj.py b/example/figure/1d/weno/some_help_code/02/testprj.py new file mode 100644 index 00000000..9ba11f09 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02/testprj.py @@ -0,0 +1,31 @@ +import sympy as sp + +# 定义符号变量 +i = sp.symbols('i', integer=True) +v = sp.Function('v') + +# 定义三个beta表达式 +beta0 = (13/sp.Integer(12) * (v(i) - 2*v(i+1) + v(i+2))**2 + + sp.Rational(1,4) * (3*v(i) - 4*v(i+1) + v(i+2))**2) + +beta1 = (13/sp.Integer(12) * (v(i-1) - 2*v(i) + v(i+1))**2 + + sp.Rational(1,4) * (v(i-1) - v(i+1))**2) + +beta2 = (13/sp.Integer(12) * (v(i-2) - 2*v(i-1) + v(i))**2 + + sp.Rational(1,4) * (v(i-2) - 4*v(i-1) + 3*v(i))**2) + +# 展开表达式 +beta0_expanded = sp.expand(beta0) +beta1_expanded = sp.expand(beta1) +beta2_expanded = sp.expand(beta2) + +print(f"beta0_expanded={beta0_expanded}") +print(f"beta1_expanded={beta1_expanded}") +print(f"beta2_expanded={beta2_expanded}") + +print("β0 的展开式:") +print(sp.latex(beta0_expanded)) +print("\nβ1 的展开式:") +print(sp.latex(beta1_expanded)) +print("\nβ2 的展开式:") +print(sp.latex(beta2_expanded)) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02a/testprj.py b/example/figure/1d/weno/some_help_code/02a/testprj.py new file mode 100644 index 00000000..1343b890 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02a/testprj.py @@ -0,0 +1,60 @@ +import sympy as sp + +# 1. 定义符号 +i = sp.symbols('i', integer=True) +v = sp.Function('v') + +# 2. 原始公式(目标) +beta0_original = (13/sp.Integer(12) * (v(i) - 2*v(i+1) + v(i+2))**2 + + sp.Rational(1,4) * (3*v(i) - 4*v(i+1) + v(i+2))**2) + +# 3. 展开式(已知条件) +beta0_expanded = sp.expand(beta0_original) +print("已知展开式:") +print(beta0_expanded) +print("\n" + "-"*80 + "\n") + +# 4. 逆向求解:假设未知线性表达式 +a, b = sp.symbols('a b') # 系数 +c1, c2, c3 = sp.symbols('c1 c2 c3') # 第一个线性表达式的系数 +d1, d2, d3 = sp.symbols('d1 d2 d3') # 第二个线性表达式的系数 + +# 设未知形式: a*(c1*v[i] + c2*v[i+1] + c3*v[i+2])**2 + b*(d1*v[i] + d2*v[i+1] + d3*v[i+2])**2 +unknown_form = a*(c1*v(i) + c2*v(i+1) + c3*v(i+2))**2 + b*(d1*v(i) + d2*v(i+1) + d3*v(i+2))**2 +unknown_expanded = sp.expand(unknown_form) + +# 5. 建立方程:比较同类项系数 +variables = [v(i)**2, v(i+1)**2, v(i+2)**2, + v(i)*v(i+1), v(i)*v(i+2), v(i+1)*v(i+2)] + +equations = [] +for var in variables: + # 获取两边系数并建立等式 + coeff_original = sp.expand(beta0_expanded).coeff(var) + coeff_unknown = unknown_expanded.coeff(var) + equations.append(sp.Eq(coeff_unknown, coeff_original)) + +print("建立的方程组:") +for eq in equations: + print(f" {eq}") + +# 6. 求解(我们知道应该有多个解,添加约束条件) +# 添加约束:系数为有理数,且第二个表达式首项系数为1(消除缩放 ambiguity) +constraints = [sp.Eq(d1, 1), sp.Eq(a, sp.Rational(13,12)), sp.Eq(b, sp.Rational(1,4))] +# 实际上更简单的方法是:直接匹配我们的预期模式 + +print("\n" + "-"*80 + "\n") +print("逆向求解结果:") + +# 更直接的方法:通过模式匹配求解 +print("β0 的平方和分解:") +# 提取 v(i), v(i+1), v(i+2) 的系数 +L1 = v(i) - 2*v(i+1) + v(i+2) +L2 = 3*v(i) - 4*v(i+1) + v(i+2) + +print(f" 第一项: {sp.Rational(13,12)} × ({L1})²") +print(f" 第二项: {sp.Rational(1,4)} × ({L2})²") + +# 验证展开是否一致 +recovered = sp.expand(sp.Rational(13,12)*L1**2 + sp.Rational(1,4)*L2**2) +print(f"\n 验证 - 重建的展开式与原始一致: {recovered == beta0_expanded}") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02aa/testprj.py b/example/figure/1d/weno/some_help_code/02aa/testprj.py new file mode 100644 index 00000000..033bc20e --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02aa/testprj.py @@ -0,0 +1,76 @@ +import sympy as sp + +# 定义符号 +i = sp.symbols('i', integer=True) +v = sp.Function('v') + +# 定义四个连续点的函数值 +v0 = v(i) +v1 = v(i+1) +v2 = v(i+2) +v3 = v(i+3) + +# 定义三个线性表达式 +L1 = v0 - 3*v1 + 3*v2 - v3 # 三阶差分 +L2 = 2*v0 - 5*v1 + 4*v2 - v3 # WENO专用模板 +L3 = 43*v0 - 69*v1 + 33*v2 - 7*v3 # 高阶修正项 + +# 定义完整的β₀表达式 +beta0 = (sp.Rational(1043,960) * L1**2 + + sp.Rational(13,12) * L2**2 + + sp.Rational(1,288) * L3 * L1 + + sp.Rational(1,576) * L3**2) + +print("="*80) +print("β₀ 原始表达式结构") +print("="*80) +print(f"项1: 1043/960 × ({L1})²") +print(f"项2: 13/12 × ({L2})²") +print(f"项3: 1/288 × ({L3}) × ({L1})") +print(f"项4: 1/576 × ({L3})²") +print("\n" + "="*80) +print("完全展开式") +print("="*80) + +# 完全展开 +beta0_expanded = sp.expand(beta0) + +# 输出LaTeX公式 +latex_output = sp.latex(beta0_expanded) +print(f"LaTeX公式:") +print(f"\\[") +print(f"\\beta_0 = {latex_output}") +print(f"\\]") + +# 为了更清晰地显示,按变量分组 +print("\n" + "="*80) +print("按变量分组的展开式") +print("="*80) + +# 收集同类项 +terms = { + 'v0²': beta0_expanded.coeff(v0**2), + 'v1²': beta0_expanded.coeff(v1**2), + 'v2²': beta0_expanded.coeff(v2**2), + 'v3²': beta0_expanded.coeff(v3**2), + 'v0v1': beta0_expanded.coeff(v0*v1), + 'v0v2': beta0_expanded.coeff(v0*v2), + 'v0v3': beta0_expanded.coeff(v0*v3), + 'v1v2': beta0_expanded.coeff(v1*v2), + 'v1v3': beta0_expanded.coeff(v1*v3), + 'v2v3': beta0_expanded.coeff(v2*v3), +} + +for term, coeff in terms.items(): + if coeff != 0: + print(f"{term}: {coeff} = {float(coeff):.6f}") + +# 输出完整的数学表达式 +print("\n" + "="*80) +print("完整的数学表达式") +print("="*80) +print("\\beta_0 = ") +for term, coeff in terms.items(): + if coeff != 0: + sign = "+" if coeff > 0 else "" + print(f" {sign}{coeff} \\cdot {term.replace('v', 'v_{i+').replace('²', '}^2').replace('v0', 'v_i').replace('v1', 'v_{i+1}').replace('v2', 'v_{i+2}').replace('v3', 'v_{i+3}')}") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02b/testprj.py b/example/figure/1d/weno/some_help_code/02b/testprj.py new file mode 100644 index 00000000..f4ae070b --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02b/testprj.py @@ -0,0 +1,296 @@ +import sympy as sp + +# 定义符号 +i = sp.symbols('i', integer=True) +v = sp.Function('v') + +# 输入:已知的展开式(不给出原始表达式) +beta0_expanded = (10*v(i)**2/3 - 31*v(i)*v(i+1)/3 + 11*v(i)*v(i+2)/3 + + 25*v(i+1)**2/3 - 19*v(i+1)*v(i+2)/3 + 4*v(i+2)**2/3) + +beta1_expanded = (13*v(i)**2/3 - 13*v(i)*v(i-1)/3 - 13*v(i)*v(i+1)/3 + + 4*v(i-1)**2/3 + 5*v(i-1)*v(i+1)/3 + 4*v(i+1)**2/3) + +beta2_expanded = (10*v(i)**2/3 + 11*v(i)*v(i-2)/3 - 31*v(i)*v(i-1)/3 + + 4*v(i-2)**2/3 - 19*v(i-2)*v(i-1)/3 + 25*v(i-1)**2/3) + +def solve_sos_decomposition(expanded_expr, variables): + """ + 求解形如 c1*L1^2 + c2*L2^2 的SOS分解 + + 参数: + expanded_expr: 展开后的表达式 + variables: 变量列表,如 [v(i), v(i+1), v(i+2)] + + 返回: + 分解结果列表 [(c1, L1), (c2, L2)] + """ + print(f"\n{'='*80}") + print(f"求解变量的SOS分解: {variables}") + print(f"{'='*80}") + + # 1. 构造对称矩阵Q,使得 expr = v^T * Q * v + n = len(variables) + Q = sp.zeros(n, n) + + print("\n步骤1: 构造二次型矩阵 Q") + print("-" * 40) + + # 对角线元素 + for i in range(n): + coeff = expanded_expr.coeff(variables[i]**2) + Q[i, i] = coeff + print(f"Q[{i},{i}] = {variables[i]}² 的系数 = {coeff}") + + # 非对角线元素(表达式中 2*x*y 的系数对应 Q[i,j] + Q[j,i] = 2*Q[i,j]) + for i in range(n): + for j in range(i+1, n): + coeff = expanded_expr.coeff(variables[i]*variables[j]) + # 由于表达式中是 var_i*var_j,矩阵中对应 2*Q[i,j] + Q[i, j] = coeff / 2 + Q[j, i] = coeff / 2 + print(f"Q[{i},{j}] = Q[{j},{i}] = {variables[i]}*{variables[j]} 系数/2 = {coeff}/2 = {coeff/2}") + + print(f"\n得到的对称矩阵 Q:") + sp.pprint(Q) + + # 2. 特征值分解(理论上有两个非零特征值) + print("\n步骤2: 矩阵的特征值分解") + print("-" * 40) + eigenvals = Q.eigenvals() + print("特征值及其重数:") + for val, mult in eigenvals.items(): + print(f" λ = {sp.nsimplify(val)} (重数: {mult})") + + # 3. 寻找秩-1分解 + # 理论上 Q = c1*w1*w1^T + c2*w2*w2^T + print("\n步骤3: 求解秩-1分解") + print("-" * 40) + + # 方法:通过比较法直接求解 + # 设未知线性表达式: L1 = a1*var0 + a2*var1 + a3*var2 + # 设未知线性表达式: L2 = b1*var0 + b2*var1 + b3*var2 + # 则 Q = c1*[a1,a2,a3]^T*[a1,a2,a3] + c2*[b1,b2,b3]^T*[b1,b2,b3] + + # 未知系数 + c1, c2 = sp.symbols('c1 c2') + a1, a2, a3 = sp.symbols('a1 a2 a3') + b1, b2, b3 = sp.symbols('b1 b2 b3') + + # 构造秩-1矩阵 + w1 = sp.Matrix([a1, a2, a3]) + w2 = sp.Matrix([b1, b2, b3]) + R1 = c1 * (w1 * w1.T) + R2 = c2 * (w2 * w2.T) + R = R1 + R2 + + print("假设分解形式: Q = c1*[a1,a2,a3]ᵀ[a1,a2,a3] + c2*[b1,b2,b3]ᵀ[b1,b2,b3]") + + # 4. 比较矩阵元素,建立方程组 + equations = [] + for i_idx in range(n): + for j_idx in range(n): + equations.append(sp.Eq(R[i_idx, j_idx], Q[i_idx, j_idx])) + + print(f"\n建立 {len(equations)} 个方程:") + for idx, eq in enumerate(equations[:6]): # 显示前6个 + print(f" 方程 {idx+1}: {eq}") + if len(equations) > 6: + print(f" ... 还有 {len(equations)-6} 个方程") + + # 5. 添加约束条件求解 + # 约束1: 系数是有理数 + # 约束2: 消除缩放歧义(令某些系数为特定值) + # 约束3: c1, c2 应为正数(因为是平方和) + + print("\n步骤4: 求解方程组(添加约束消除缩放歧义)") + print("-" * 40) + print("添加约束: b1=1(固定第一个系数)") + + # 实际求解时使用数值方法先找到近似解 + equations_constrained = equations + [sp.Eq(b1, 1)] + + # 使用 nsolve 寻找数值解(需要提供初始值) + print("\n使用数值求解作为初始猜测...") + try: + # 初始猜测 + initial_guess = { + a1: 1, a2: -2, a3: 1, + b1: 1, b2: -2, b3: 1, + c1: 1, c2: 1 + } + + # 选择9个独立方程求解9个未知数 + sol = sp.nsolve(equations_constrained[:9], + [a1, a2, a3, b1, b2, b3, c1, c2], + [1, -2, 1, 1, -2, 1, 1, 1], + tol=1e-14, maxsteps=100) + + print(f"数值解: {sol}") + + # 根据数值解模式,推断符号解 + # 观察数值解的模式,手动构造符号解 + + except Exception as e: + print(f"数值求解遇到错误: {e}") + print("切换到符号模式匹配...") + + # 6. 使用启发式方法:寻找整数/有理数解 + # 观察矩阵Q的结构,尝试匹配模式 + + print("\n步骤5: 启发式模式匹配") + print("-" * 40) + + # 对于beta0,观察系数模式 + # Q = [[10/3, -31/6, 11/6], + # [-31/6, 25/3, -19/6], + # [11/6, -19/6, 4/3]] + + # 尝试寻找形如 (1, -2, 1) 的模式 + # 这是差分算子的典型模式 + + test_vector1 = sp.Matrix([1, -2, 1]) # 二阶差分 + test_vector2 = sp.Matrix([1, -1, 0]) # 一阶差分 + test_vector3 = sp.Matrix([3, -4, 1]) # WENO的特定组合 + + # 测试哪个向量能匹配 + print("测试候选向量:") + + candidates = [ + ("[1, -2, 1] (二阶差分)", sp.Matrix([1, -2, 1])), + ("[1, -1, 0]", sp.Matrix([1, -1, 0])), + ("[3, -4, 1] (WENO专用)", sp.Matrix([3, -4, 1])), + ] + + for name, vec in candidates: + # 计算 rank(Q - λ*vv^T) + vvt = vec * vec.T + print(f"\n 测试向量 {name}:") + sp.pprint(vec.T) + + # 尝试用最小二乘拟合求解λ + # 对于矩阵的每个非零元素位置 + ratios = [] + for ii in range(n): + for jj in range(n): + if vvt[ii, jj] != 0: + ratios.append(Q[ii, jj] / vvt[ii, jj]) + + if ratios: + avg_ratio = sum(ratios) / len(ratios) + print(f" 平均比例系数: {sp.nsimplify(avg_ratio)}") + + # 验证剩余矩阵 + residual = Q - sp.nsimplify(avg_ratio) * vvt + print(f" 剩余矩阵的秩: {residual.rank()}") + + if residual.rank() == 1: + print(f" ✓ 成功!剩余矩阵秩为1,可以继续分解") + # 寻找第二个向量 + # 剩余矩阵应为 c2 * w2 * w2^T + for name2, vec2 in candidates: + if name2 != name: + vvt2 = vec2 * vec2.T + ratios2 = [] + for ii in range(n): + for jj in range(n): + if vvt2[ii, jj] != 0 and residual[ii, jj] != 0: + ratios2.append(residual[ii, jj] / vvt2[ii, jj]) + if ratios2: + avg_ratio2 = sum(ratios2) / len(ratios2) + residual2 = residual - sp.nsimplify(avg_ratio2) * vvt2 + if residual2.norm() < 1e-10: + print(f" 第二个向量 {name2}, 系数: {sp.nsimplify(avg_ratio2)}") + return [(sp.nsimplify(avg_ratio), vec), (sp.nsimplify(avg_ratio2), vec2)] + + # 如果没有找到完美匹配,使用特征值方法 + print("\n 使用特征值分解方法...") + eigenvecs = Q.eigenvects() + + sos_decomp = [] + for val_mult in eigenvecs: + val = sp.nsimplify(val_mult[0]) + mult = val_mult[1] + if abs(val) > 1e-10: + # 获取特征向量 + vec = val_mult[2][0] # 第一个特征向量 + # 归一化 + vec_simplified = sp.nsimplify(vec / sp.sqrt(vec.dot(vec))) + sos_decomp.append((val, vec_simplified)) + + return sos_decomp + +def format_sos_result(decomp, variables): + """格式化SOS分解结果为可读形式""" + if not decomp: + return "未找到分解" + + result = [] + for idx, (coeff, vec) in enumerate(decomp): + # 构建线性表达式 + terms = [] + for var_idx, var in enumerate(variables): + if vec[var_idx] != 0: + term = sp.nsimplify(vec[var_idx]) * var + terms.append(str(term)) + + linear_expr = " + ".join(terms).replace("+ -", "- ") + result.append(f" 项{idx+1}: {sp.nsimplify(coeff)} × ({linear_expr})²") + + return "\n".join(result) + +# 对每个beta进行分解 +print("\n" + "="*80) +print("SOS分解逆向求解") +print("="*80) + +# β0分解 +vars0 = [v(i), v(i+1), v(i+2)] +decomp0 = solve_sos_decomposition(beta0_expanded, vars0) +print("\n最终结果:") +print(format_sos_result(decomp0, vars0)) + +# β1分解 +vars1 = [v(i-1), v(i), v(i+1)] +decomp1 = solve_sos_decomposition(beta1_expanded, vars1) +print("\nβ1的SOS分解:") +print(format_sos_result(decomp1, vars1)) + +# β2分解 +vars2 = [v(i-2), v(i-1), v(i)] +decomp2 = solve_sos_decomposition(beta2_expanded, vars2) +print("\nβ2的SOS分解:") +print(format_sos_result(decomp2, vars2)) + +# 验证分解 +print("\n="*80) +print("验证分解正确性") +print("="*80) + +def verify_decomposition(original_expanded, decomp, variables): + """验证分解是否正确重构原式""" + if not decomp: + return False + + reconstructed = 0 + for coeff, vec in decomp: + # 构造线性表达式 + linear = sum(sp.nsimplify(vec[idx]) * var for idx, var in enumerate(variables)) + reconstructed += sp.nsimplify(coeff) * linear**2 + + reconstructed = sp.expand(reconstructed) + diff = sp.simplify(reconstructed - original_expanded) + return diff == 0 + +# 验证每个分解 +for idx, (decomp, vars_list, original) in enumerate([ + (decomp0, vars0, beta0_expanded), + (decomp1, vars1, beta1_expanded), + (decomp2, vars2, beta2_expanded) +], 0): + print(f"\nβ{idx} 验证:") + is_correct = verify_decomposition(original, decomp, vars_list) + print(f" 分解正确: {is_correct}") + + if is_correct: + print(f" 重建表达式: {sp.expand(sum(sp.nsimplify(coeff)*sum(sp.nsimplify(vec[idx])*var for idx, var in enumerate(vars_list))**2 for coeff, vec in decomp))}") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02c/testprj.py b/example/figure/1d/weno/some_help_code/02c/testprj.py new file mode 100644 index 00000000..fc71a375 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02c/testprj.py @@ -0,0 +1,21 @@ +from sympy import symbols, Rational, expand, latex + +# 定义符号变量 +v_im2, v_im1, v_i, v_ip1, v_ip2 = symbols('v_{i-2} v_{i-1} v_i v_{i+1} v_{i+2}') + +# β0 的表达式 +beta0_expr = Rational(13, 12) * (v_i - 2 * v_ip1 + v_ip2)**2 + Rational(1, 4) * (3 * v_i - 4 * v_ip1 + v_ip2)**2 +beta0_expanded = expand(beta0_expr) + +# β1 的表达式 +beta1_expr = Rational(13, 12) * (v_im1 - 2 * v_i + v_ip1)**2 + Rational(1, 4) * (v_im1 - v_ip1)**2 +beta1_expanded = expand(beta1_expr) + +# β2 的表达式 +beta2_expr = Rational(13, 12) * (v_im2 - 2 * v_im1 + v_i)**2 + Rational(1, 4) * (v_im2 - 4 * v_im1 + 3 * v_i)**2 +beta2_expanded = expand(beta2_expr) + +# 输出 LaTeX 公式 +print("β0 = " + latex(beta0_expanded)) +print("β1 = " + latex(beta1_expanded)) +print("β2 = " + latex(beta2_expanded)) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02d/formulas.json b/example/figure/1d/weno/some_help_code/02d/formulas.json new file mode 100644 index 00000000..9033ca52 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02d/formulas.json @@ -0,0 +1,5 @@ +{ + "beta0": "Rational(13, 12) * (v_i - 2 * v_ip1 + v_ip2)**2 + Rational(1, 4) * (3 * v_i - 4 * v_ip1 + v_ip2)**2", + "beta1": "Rational(13, 12) * (v_im1 - 2 * v_i + v_ip1)**2 + Rational(1, 4) * (v_im1 - v_ip1)**2", + "beta2": "Rational(13, 12) * (v_im2 - 2 * v_im1 + v_i)**2 + Rational(1, 4) * (v_im2 - 4 * v_im1 + 3 * v_i)**2" +} \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02d/testprj.py b/example/figure/1d/weno/some_help_code/02d/testprj.py new file mode 100644 index 00000000..5fb4a72f --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02d/testprj.py @@ -0,0 +1,33 @@ +import json +from sympy import symbols, Rational, expand, latex, sympify + +# 定义符号变量(假设输入公式中使用这些符号) +v_im2, v_im1, v_i, v_ip1, v_ip2 = symbols('v_{i-2} v_{i-1} v_i v_{i+1} v_{i+2}') + +def read_and_expand_formulas(file_path): + """ + 从JSON文件中读取公式表达式,展开并输出LaTeX。 + + 输入文件格式:JSON对象,键为"beta0", "beta1", "beta2",值为SymPy兼容的字符串表达式。 + 示例文件内容: + { + "beta0": "Rational(13, 12) * (v_i - 2 * v_ip1 + v_ip2)**2 + Rational(1, 4) * (3 * v_i - 4 * v_ip1 + v_ip2)**2", + "beta1": "Rational(13, 12) * (v_im1 - 2 * v_i + v_ip1)**2 + Rational(1, 4) * (v_im1 - v_ip1)**2", + "beta2": "Rational(13, 12) * (v_im2 - 2 * v_im1 + v_i)**2 + Rational(1, 4) * (v_im2 - 4 * v_im1 + 3 * v_i)**2" + } + """ + with open(file_path, 'r') as f: + formulas = json.load(f) + + results = {} + for name, expr_str in formulas.items(): + expr = sympify(expr_str) + expanded = expand(expr) + results[name] = latex(expanded) + + # 输出LaTeX公式 + for name, latex_str in results.items(): + print(f"{name} = " + latex_str) + +# 使用示例:替换为实际文件路径 +read_and_expand_formulas('formulas.json') \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02e/formulas.json b/example/figure/1d/weno/some_help_code/02e/formulas.json new file mode 100644 index 00000000..9033ca52 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02e/formulas.json @@ -0,0 +1,5 @@ +{ + "beta0": "Rational(13, 12) * (v_i - 2 * v_ip1 + v_ip2)**2 + Rational(1, 4) * (3 * v_i - 4 * v_ip1 + v_ip2)**2", + "beta1": "Rational(13, 12) * (v_im1 - 2 * v_i + v_ip1)**2 + Rational(1, 4) * (v_im1 - v_ip1)**2", + "beta2": "Rational(13, 12) * (v_im2 - 2 * v_im1 + v_i)**2 + Rational(1, 4) * (v_im2 - 4 * v_im1 + 3 * v_i)**2" +} \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02e/testprj.py b/example/figure/1d/weno/some_help_code/02e/testprj.py new file mode 100644 index 00000000..49c91fa5 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02e/testprj.py @@ -0,0 +1,48 @@ +import json +from sympy import symbols, Rational, expand, latex, sympify + +# 定义符号变量(使用LaTeX下标名称) +v_im2, v_im1, v_i, v_ip1, v_ip2 = symbols('v_{i-2} v_{i-1} v_i v_{i+1} v_{i+2}') + +def read_and_expand_formulas(file_path): + """ + 从JSON文件中读取公式表达式,展开并输出指定的LaTeX格式。 + + 输入文件格式:JSON对象,键为"beta0", "beta1", "beta2",值为SymPy兼容的字符串表达式。 + 示例文件内容: + { + "beta0": "Rational(13, 12) * (v_i - 2 * v_ip1 + v_ip2)**2 + Rational(1, 4) * (3 * v_i - 4 * v_ip1 + v_ip2)**2", + "beta1": "Rational(13, 12) * (v_im1 - 2 * v_i + v_ip1)**2 + Rational(1, 4) * (v_im1 - v_ip1)**2", + "beta2": "Rational(13, 12) * (v_im2 - 2 * v_im1 + v_i)**2 + Rational(1, 4) * (v_im2 - 4 * v_im1 + 3 * v_i)**2" + } + """ + with open(file_path, 'r') as f: + formulas = json.load(f) + + # 定义局部变量字典,用于sympify解析表达式 + locals_dict = { + 'v_im2': v_im2, + 'v_im1': v_im1, + 'v_i': v_i, + 'v_ip1': v_ip1, + 'v_ip2': v_ip2 + } + + results = {} + for name, expr_str in formulas.items(): + expr = sympify(expr_str, locals=locals_dict) + expanded = expand(expr) + results[name] = latex(expanded) + + # 输出指定的LaTeX数组格式 + output = r'\begin{array}{l}' + '\n' + for i, name in enumerate(['beta0', 'beta1', 'beta2']): + output += f'β{name[4:]} = {results[name]}' + if i < 2: + output += r'\\' + output += '\n' + output += r'\end{array}' + print(output) + +# 使用示例:替换为实际文件路径 +read_and_expand_formulas('formulas.json') \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/03/testprj.py b/example/figure/1d/weno/some_help_code/03/testprj.py new file mode 100644 index 00000000..80d7d197 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/03/testprj.py @@ -0,0 +1,87 @@ +import sympy as sp + +class LatexParser: + def __init__(self): + self.symbols = {} + + def register_symbols(self, symbol_list): + """预定义符号""" + for sym in symbol_list: + self.symbols[sym] = sp.symbols(sym) + + def parse(self, latex_str, use_backup=False): + """ + 解析 LaTeX 字符串 + use_backup: 如果主要方法失败,是否使用备选方案 + """ + # 方法1: 尝试 sympy 的 parse_latex + try: + expr = parse_latex(latex_str) + return expr, "sympy_parse_latex" + except: + if not use_backup: + raise + + # 方法2: 转换为 sympy 友好格式 + try: + sympy_friendly = self._latex_to_sympy_friendly(latex_str) + expr = sp.sympify(sympy_friendly, locals=self.symbols) + return expr, "converted_sympify" + except: + pass + + # 方法3: 尝试 latex2sympy2 (如果安装) + try: + from latex2sympy2 import latex2sympy + expr = latex2sympy(latex_str) + return expr, "latex2sympy2" + except ImportError: + print("警告: 未安装 latex2sympy2") + except Exception: + pass + + raise ValueError(f"无法解析 LaTeX: {latex_str}") + + def _latex_to_sympy_friendly(self, latex_str): + """内部转换方法""" + # 简化的转换逻辑 + import re + + replacements = [ + (r'\\frac{(.*?)}{(.*?)}', r'(\1)/(\2)'), + (r'\\sqrt{(.*?)}', r'sqrt(\1)'), + (r'\^', r'**'), + (r'\\cdot', r'*'), + (r'\\times', r'*'), + (r'\\div', r'/'), + ] + + result = latex_str + for pattern, replacement in replacements: + result = re.sub(pattern, replacement, result) + + # 移除括号 + result = result.replace('{', '(').replace('}', ')') + + return result + +# 使用示例 +parser = LatexParser() +parser.register_symbols(['x', 'y', 'z', 'alpha', 'beta']) + +test_cases = [ + (r"x^2 + y^2", "简单幂运算"), + (r"\frac{x + 1}{y - 2}", "分数"), + (r"\sin(\alpha) \cdot \cos(\beta)", "三角函数"), +] + +for latex_str, description in test_cases: + try: + expr, method = parser.parse(latex_str, use_backup=True) + print(f"{description}:") + print(f" LaTeX: {latex_str}") + print(f" 方法: {method}") + print(f" 结果: {expr}") + print() + except Exception as e: + print(f"{description} 解析失败: {e}") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/03a/testprj.py b/example/figure/1d/weno/some_help_code/03a/testprj.py new file mode 100644 index 00000000..6555a1ed --- /dev/null +++ b/example/figure/1d/weno/some_help_code/03a/testprj.py @@ -0,0 +1,108 @@ +import sympy as sp +import numpy as np + +# 定义变量 +v = sp.symbols('v0:6') # v0, v1, v2, v3, v4, v5 +# 为了对应原表达式,设定索引映射: +# v[i] -> v2 +# v[i+1] -> v3 +# v[i+2] -> v4 +# v[i-1] -> v1 +# v[i-2] -> v0 +# 我们用通用符号,之后替换 + +i = 2 # 中间索引为2,则 v[i]=v2, v[i+1]=v3, v[i+2]=v4, v[i-1]=v1, v[i-2]=v0 + +# 定义原表达式 +beta0_expr = 10*v[2]**2/3 - 31*v[2]*v[3]/3 + 11*v[2]*v[4]/3 + 25*v[3]**2/3 - 19*v[3]*v[4]/3 + 4*v[4]**2/3 +beta1_expr = 13*v[2]**2/3 - 13*v[2]*v[1]/3 - 13*v[2]*v[3]/3 + 4*v[1]**2/3 + 5*v[1]*v[3]/3 + 4*v[3]**2/3 +beta2_expr = 10*v[2]**2/3 + 11*v[2]*v[0]/3 - 31*v[2]*v[1]/3 + 4*v[0]**2/3 - 19*v[0]*v[1]/3 + 25*v[1]**2/3 + +# 将二次型转换为矩阵形式 +def quad_form_to_matrix(expr, var_list): + """返回二次型 expr 关于变量 var_list 的对称矩阵 A""" + n = len(var_list) + A = sp.zeros(n, n) + for i in range(n): + for j in range(i, n): + coeff = expr.coeff(var_list[i]*var_list[j]) + if i == j: + A[i,j] = coeff + else: + # 交叉项系数,二次型中 x_i x_j 系数对应矩阵 (i,j) 和 (j,i) 各一半 + A[i,j] = coeff/2 + A[j,i] = coeff/2 + return A + +# 对 beta0 +var0 = [v[2], v[3], v[4]] +A0 = quad_form_to_matrix(beta0_expr, var0) +print("A0 =", A0) + +# 对 beta1 +var1 = [v[1], v[2], v[3]] +A1 = quad_form_to_matrix(beta1_expr, var1) +print("A1 =", A1) + +# 对 beta2 +var2 = [v[0], v[1], v[2]] +A2 = quad_form_to_matrix(beta2_expr, var2) +print("A2 =", A2) + +# 特征值分解找平方和 +def sos_from_matrix(A, vars): + """将半正定矩阵A分解为 sum_i (linear_form)^2""" + # 对称矩阵的特征值分解 + A_np = np.array(A, dtype=float) + eigvals, eigvecs = np.linalg.eigh(A_np) + # 只取正特征值 + sos_terms = [] + for val, vec in zip(eigvals, eigvecs.T): + if abs(val) > 1e-10: + linear = sum(float(c)*x for c, x in zip(vec, vars)) + sos_terms.append((val, linear)) + return sos_terms + +print("\n--- Beta0 SOS ---") +sos0 = sos_from_matrix(A0, var0) +for val, lin in sos0: + print(f"{val:.6f} * ({lin})^2") + +print("\n--- Beta1 SOS ---") +sos1 = sos_from_matrix(A1, var1) +for val, lin in sos1: + print(f"{val:.6f} * ({lin})^2") + +print("\n--- Beta2 SOS ---") +sos2 = sos_from_matrix(A2, var2) +for val, lin in sos2: + print(f"{val:.6f} * ({lin})^2") + +# 也可以尝试用符号 Cholesky 分解(但要求正定,这里可能半正定) +# 用 sympy 的 LDL 分解(对半正定有效) +def sos_ldl(A, vars): + """LDL^T 分解得到平方和""" + L, D = A.LDLdecomposition() + # A = L * D * L.T + # 那么 x^T A x = (sqrt(D) L^T x)^T (sqrt(D) L^T x) + n = A.rows + terms = [] + for j in range(n): + if D[j, j] != 0: + linear = sum(L[i, j]*vars[i] for i in range(n)) + terms.append((D[j, j], linear)) + return terms + +print("\n--- 使用 LDL 分解 ---") +print("Beta0:") +terms0 = sos_ldl(A0, var0) +for coeff, lin in terms0: + print(f"{coeff} * ({lin})^2") +print("Beta1:") +terms1 = sos_ldl(A1, var1) +for coeff, lin in terms1: + print(f"{coeff} * ({lin})^2") +print("Beta2:") +terms2 = sos_ldl(A2, var2) +for coeff, lin in terms2: + print(f"{coeff} * ({lin})^2") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/03b/testprj.py b/example/figure/1d/weno/some_help_code/03b/testprj.py new file mode 100644 index 00000000..a397b9f9 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/03b/testprj.py @@ -0,0 +1,220 @@ +import sympy as sp +import numpy as np + +# 定义变量 +v = sp.symbols('v0:6') # v0, v1, v2, v3, v4, v5 +# 索引映射: +# v[i] -> v2 +# v[i+1] -> v3 +# v[i+2] -> v4 +# v[i-1] -> v1 +# v[i-2] -> v0 + +# 定义原表达式 +beta0_expr = 10*v[2]**2/3 - 31*v[2]*v[3]/3 + 11*v[2]*v[4]/3 + 25*v[3]**2/3 - 19*v[3]*v[4]/3 + 4*v[4]**2/3 +beta1_expr = 13*v[2]**2/3 - 13*v[2]*v[1]/3 - 13*v[2]*v[3]/3 + 4*v[1]**2/3 + 5*v[1]*v[3]/3 + 4*v[3]**2/3 +beta2_expr = 10*v[2]**2/3 + 11*v[2]*v[0]/3 - 31*v[2]*v[1]/3 + 4*v[0]**2/3 - 19*v[0]*v[1]/3 + 25*v[1]**2/3 + +# 将二次型转换为矩阵形式 +def quad_form_to_matrix(expr, var_list): + """返回二次型 expr 关于变量 var_list 的对称矩阵 A""" + n = len(var_list) + A = sp.zeros(n, n) + for i in range(n): + for j in range(i, n): + coeff = expr.coeff(var_list[i]*var_list[j]) + if i == j: + A[i,j] = coeff + else: + # 交叉项系数,二次型中 x_i x_j 系数对应矩阵 (i,j) 和 (j,i) 各一半 + A[i,j] = coeff/2 + A[j,i] = coeff/2 + return A + +# 对 beta0 +var0 = [v[2], v[3], v[4]] +A0 = quad_form_to_matrix(beta0_expr, var0) +print("A0 =", A0) + +# 对 beta1 +var1 = [v[1], v[2], v[3]] +A1 = quad_form_to_matrix(beta1_expr, var1) +print("\nA1 =", A1) + +# 对 beta2 +var2 = [v[0], v[1], v[2]] +A2 = quad_form_to_matrix(beta2_expr, var2) +print("\nA2 =", A2) + +# 方法1:使用正交对角化(特征值分解)得到有理数形式 +def sos_rational_from_matrix(A, vars): + """将半正定矩阵A分解为有理数形式的平方和""" + # 计算特征值和特征向量(符号计算) + eig_data = A.eigenvects() + + sos_terms = [] + for eigval, multiplicity, eigvecs in eig_data: + if eigval > 0: # 只取正特征值 + for vec in eigvecs: + # 将特征向量化为最简整数形式 + vec = sp.Matrix(vec) + # 找到最小公倍数使得所有系数为整数 + denoms = [sp.fraction(sp.simplify(c))[1] for c in vec if c != 0] + lcm_denom = 1 + for d in denoms: + if d != 1: + lcm_denom = sp.lcm(lcm_denom, d) + + # 乘以最小公倍数得到整数系数 + vec_int = vec * lcm_denom + # 提取系数的最大公约数 + coeffs = [vec_int[i] for i in range(vec_int.rows)] + gcd_coeff = abs(sp.gcd(coeffs)) + if gcd_coeff > 1: + vec_int = vec_int / gcd_coeff + + # 构造线性组合 + linear_expr = sum(vec_int[i] * vars[i] for i in range(len(vars))) + + # 计算系数 + # 验证:vec^T A vec = eigval * vec^T vec + # 但我们需要的是 eigval * (vec^T x)^2 / (vec^T vec) + vec_norm2 = sum(c**2 for c in vec) + coefficient = eigval / vec_norm2 * (lcm_denom / gcd_coeff)**2 + + # 简化系数为分数形式 + coefficient = sp.nsimplify(coefficient) + + sos_terms.append((coefficient, linear_expr)) + + return sos_terms + +# 方法2:使用完成平方的方法 +def complete_square(expr, vars): + """使用完成平方的方法得到SOS表示""" + # 这是一个启发式方法,尝试常见的线性组合模式 + # 对于WENO格式,通常的形式是 a*(p*v[i] + q*v[i+1] + r*v[i+2])^2 + b*(s*v[i] + t*v[i+1] + u*v[i+2])^2 + + # 对于beta0,我们知道形式应该是: + # c1*(v[i] - 2*v[i+1] + v[i+2])^2 + c2*(3*v[i] - 4*v[i+1] + v[i+2])^2 + # 让我们用符号求解系数 + + if vars == var0: # beta0 + # 尝试形式: c1*(a*v2 + b*v3 + c*v4)^2 + c2*(d*v2 + e*v3 + f*v4)^2 + c1, c2, a, b, c, d, e, f = sp.symbols('c1 c2 a b c d e f') + target = c1*(a*v[2] + b*v[3] + c*v[4])**2 + c2*(d*v[2] + e*v[3] + f*v[4])**2 + + # 展开并比较系数 + target_expanded = sp.expand(target) + + # 收集系数方程 + coeff_eqs = [] + # v2^2 系数 + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[2]**2), beta0_expr.coeff(v[2]**2))) + # v3^2 系数 + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[3]**2), beta0_expr.coeff(v[3]**2))) + # v4^2 系数 + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[4]**2), beta0_expr.coeff(v[4]**2))) + # v2*v3 系数 + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[2]*v[3]), beta0_expr.coeff(v[2]*v[3]))) + # v2*v4 系数 + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[2]*v[4]), beta0_expr.coeff(v[2]*v[4]))) + # v3*v4 系数 + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[3]*v[4]), beta0_expr.coeff(v[3]*v[4]))) + + # 添加归一化约束以减少自由度 + # 让第一个线性组合的系数为简单整数,比如 (1, -2, 1) + coeff_eqs.append(sp.Eq(a, 1)) + coeff_eqs.append(sp.Eq(b, -2)) + coeff_eqs.append(sp.Eq(c, 1)) + + # 求解 + sol = sp.nsolve(coeff_eqs, [c1, c2, a, b, c, d, e, f], [13/12, 1/4, 1, -2, 1, 3, -4, 1]) + + return sol + + elif vars == var1: # beta1 + # 形式: c1*(v1 - 2*v2 + v3)^2 + c2*(v1 - v3)^2 + c1, c2 = sp.symbols('c1 c2') + target = c1*(v[1] - 2*v[2] + v[3])**2 + c2*(v[1] - v[3])**2 + target_expanded = sp.expand(target) + + # 收集系数方程 + coeff_eqs = [] + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[1]**2), beta1_expr.coeff(v[1]**2))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[2]**2), beta1_expr.coeff(v[2]**2))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[3]**2), beta1_expr.coeff(v[3]**2))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[1]*v[2]), beta1_expr.coeff(v[1]*v[2]))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[1]*v[3]), beta1_expr.coeff(v[1]*v[3]))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[2]*v[3]), beta1_expr.coeff(v[2]*v[3]))) + + # 求解 + sol = sp.solve(coeff_eqs, [c1, c2]) + return sol + + elif vars == var2: # beta2 + # 形式: c1*(v0 - 2*v1 + v2)^2 + c2*(v0 - 4*v1 + 3*v2)^2 + c1, c2 = sp.symbols('c1 c2') + target = c1*(v[0] - 2*v[1] + v[2])**2 + c2*(v[0] - 4*v[1] + 3*v[2])**2 + target_expanded = sp.expand(target) + + # 收集系数方程 + coeff_eqs = [] + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[0]**2), beta2_expr.coeff(v[0]**2))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[1]**2), beta2_expr.coeff(v[1]**2))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[2]**2), beta2_expr.coeff(v[2]**2))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[0]*v[1]), beta2_expr.coeff(v[0]*v[1]))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[0]*v[2]), beta2_expr.coeff(v[0]*v[2]))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[1]*v[2]), beta2_expr.coeff(v[1]*v[2]))) + + # 求解 + sol = sp.solve(coeff_eqs, [c1, c2]) + return sol + +print("\n=== 使用完成平方的方法 ===") +print("\n--- Beta0 ---") +sol0 = complete_square(beta0_expr, var0) +if sol0: + print("系数解:", sol0) + # 重构表达式 + if isinstance(sol0, list): + c1_val, c2_val = sol0[0][0], sol0[0][1] + beta0_reconstructed = c1_val*(v[2] - 2*v[3] + v[4])**2 + c2_val*(3*v[2] - 4*v[3] + v[4])**2 + print("重构的Beta0:", beta0_reconstructed) + print("是否等于原式:", sp.simplify(beta0_expr - beta0_reconstructed) == 0) + +print("\n--- Beta1 ---") +sol1 = complete_square(beta1_expr, var1) +if sol1: + print("系数解:", sol1) + # 重构表达式 + c1_val, c2_val = list(sol1.values())[0] + beta1_reconstructed = c1_val*(v[1] - 2*v[2] + v[3])**2 + c2_val*(v[1] - v[3])**2 + print("重构的Beta1:", beta1_reconstructed) + print("是否等于原式:", sp.simplify(beta1_expr - beta1_reconstructed) == 0) + +print("\n--- Beta2 ---") +sol2 = complete_square(beta2_expr, var2) +if sol2: + print("系数解:", sol2) + # 重构表达式 + c1_val, c2_val = list(sol2.values())[0] + beta2_reconstructed = c1_val*(v[0] - 2*v[1] + v[2])**2 + c2_val*(v[0] - 4*v[1] + 3*v[2])**2 + print("重构的Beta2:", beta2_reconstructed) + print("是否等于原式:", sp.simplify(beta2_expr - beta2_reconstructed) == 0) + +print("\n=== 总结 ===") +print("Beta0 = 13/12 * (v[i] - 2v[i+1] + v[i+2])^2 + 1/4 * (3v[i] - 4v[i+1] + v[i+2])^2") +print("Beta1 = 13/12 * (v[i-1] - 2v[i] + v[i+1])^2 + 1/4 * (v[i-1] - v[i+1])^2") +print("Beta2 = 13/12 * (v[i-2] - 2v[i-1] + v[i])^2 + 1/4 * (v[i-2] - 4v[i-1] + 3v[i])^2") + +# 验证 +print("\n=== 验证 ===") +beta0_target = sp.Rational(13,12)*(v[2] - 2*v[3] + v[4])**2 + sp.Rational(1,4)*(3*v[2] - 4*v[3] + v[4])**2 +print("Beta0 匹配:", sp.simplify(beta0_expr - beta0_target) == 0) + +beta1_target = sp.Rational(13,12)*(v[1] - 2*v[2] + v[3])**2 + sp.Rational(1,4)*(v[1] - v[3])**2 +print("Beta1 匹配:", sp.simplify(beta1_expr - beta1_target) == 0) + +beta2_target = sp.Rational(13,12)*(v[0] - 2*v[1] + v[2])**2 + sp.Rational(1,4)*(v[0] - 4*v[1] + 3*v[2])**2 +print("Beta2 匹配:", sp.simplify(beta2_expr - beta2_target) == 0) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/03c/testprj.py b/example/figure/1d/weno/some_help_code/03c/testprj.py new file mode 100644 index 00000000..c6785337 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/03c/testprj.py @@ -0,0 +1,105 @@ +from sympy import symbols, Rational, expand, simplify, nsimplify +from sympy.core.numbers import Float + +# 1. 定义符号变量 +v2, v3, v4 = symbols('v2 v3 v4', real=True) + +# 2. 配置参数(控制有理数简洁性) +DECIMAL_PRECISION = 8 # 保留8位小数(可调整,平衡精度和简洁性) +#MAX_DENOMINATOR = 1000000 # 有理数分母最大值(避免超大数字) +MAX_DENOMINATOR = 1000 + +# 3. 原始系数 +# 第一个表达式:0.255002 * (0.64295988*v2 + 0.11435482*v3 - 0.75731471*v4)^2(截断后) +coeff1_out = 0.255002 +coeff1_in = [0.642959882291173, 0.114354823786141, -0.757314706077309] + +# 第二个表达式:12.744998 * (-0.50325864*v2 + 0.80844891*v3 - 0.30519027*v4)^2(截断后) +coeff2_out = 12.744998 +coeff2_in = [-0.503258637711058, 0.808448910533936, -0.305190272822878] + +def float_to_simple_rational(num): + """ + 将浮点数转换为简洁的有理数: + 1. 截断浮点精度,消除噪声 + 2. 限制分母最大值,避免虚假大数字 + """ + # 步骤1:截断浮点数精度(保留DECIMAL_PRECISION位小数) + #truncated = round(num, DECIMAL_PRECISION) + truncated = num + # 步骤2:转换为有理数,限制分母最大值 + return Rational(truncated).limit_denominator(MAX_DENOMINATOR) + +def normalize_linear_coeffs_simple(coeffs): + """ + 优化版:将线性系数转为最小整数(基于简洁有理数) + """ + # 步骤1:转简洁有理数 + rat_coeffs = [float_to_simple_rational(c) for c in coeffs] + + # 步骤2:提取公分母,转为整数 + denoms = [c.denominator for c in rat_coeffs] + lcm_denom = 1 + for d in denoms: + lcm_denom = lcm_denom * d // gcd(lcm_denom, d) + int_coeffs = [c * lcm_denom for c in rat_coeffs] + + # 步骤3:提取最大公约数,化为最小整数 + abs_ints = [abs(int(c)) for c in int_coeffs if c != 0] + gcd_int = abs_ints[0] if abs_ints else 1 + for num in abs_ints[1:]: + gcd_int = gcd(gcd_int, num) + + # 最小整数系数 + 提取的公因子 + min_coeffs = [int(c / gcd_int) for c in int_coeffs] # 确保是整数类型 + factor_out = Rational(gcd_int, lcm_denom) + + return min_coeffs, factor_out + +# 辅助函数:GCD和LCM +def gcd(a, b): + while b: + a, b = b, a % b + return a + +# ------------------------ 处理第一个表达式 ------------------------ +min_coeffs1, factor1 = normalize_linear_coeffs_simple(coeff1_in) +rat_out1 = float_to_simple_rational(coeff1_out) +linear1 = min_coeffs1[0]*v2 + min_coeffs1[1]*v3 + min_coeffs1[2]*v4 +expr1 = rat_out1 * (factor1 **2) * (linear1** 2) +expr1_simplified = simplify(expr1) + +# ------------------------ 处理第二个表达式 ------------------------ +min_coeffs2, factor2 = normalize_linear_coeffs_simple(coeff2_in) +rat_out2 = float_to_simple_rational(coeff2_out) +linear2 = min_coeffs2[0]*v2 + min_coeffs2[1]*v3 + min_coeffs2[2]*v4 +expr2 = rat_out2 * (factor2 **2) * (linear2** 2) +expr2_simplified = simplify(expr2) + +# ------------------------ 输出结果 ------------------------ +print(f"配置:保留{DECIMAL_PRECISION}位小数,分母最大为{MAX_DENOMINATOR}") +print("\n=== 第一个表达式处理结果 ===") +print(f"平方内最小整数线性组合:{linear1}") +print(f"平方外最简有理系数:{expr1_simplified.coeff(linear1**2)}") +print(f"完整等价表达式:{expr1_simplified}") + +print("\n=== 第二个表达式处理结果 ===") +print(f"平方内最小整数线性组合:{linear2}") +print(f"平方外最简有理系数:{expr2_simplified.coeff(linear2**2)}") +print(f"完整等价表达式:{expr2_simplified}") + +# ------------------------ 等价性验证 ------------------------ +def verify_equivalence(expr_sym, coeff_out, coeff_in, v_vals=[1,1,1]): + """验证符号表达式与原始浮点表达式的数值等价性""" + # 原始浮点值 + linear_float = sum(c*v for c, v in zip(coeff_in, v_vals)) + float_val = coeff_out * (linear_float **2) + # 符号表达式值 + sym_val = expr_sym.subs({v2:v_vals[0], v3:v_vals[1], v4:v_vals[2]}).evalf() + # 误差阈值(根据截断精度调整) + error = abs(float_val - sym_val) + return error < 10**(-DECIMAL_PRECISION) + +print("\n=== 等价性验证 ===") +print(f"第一个表达式(v2=1,v3=1,v4=1):误差={verify_equivalence(expr1_simplified, coeff1_out, coeff1_in, [1,1,1])}") +print(f"第二个表达式(v2=1,v3=1,v4=1):误差={verify_equivalence(expr2_simplified, coeff2_out, coeff2_in, [1,1,1])}") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/04/testprj.py b/example/figure/1d/weno/some_help_code/04/testprj.py new file mode 100644 index 00000000..fcc083dd --- /dev/null +++ b/example/figure/1d/weno/some_help_code/04/testprj.py @@ -0,0 +1,41 @@ +from sympy import symbols, Rational, simplify, pprint, expand + +# 定义符号(v0 = v(i), v1 = v(i+1), v2 = v(i+2)) +v0, v1, v2 = symbols('v0 v1 v2') + +# beta0 的展开形式 +beta0 = (Rational(10,3)*v0**2 - Rational(31,3)*v0*v1 + Rational(11,3)*v0*v2 + + Rational(25,3)*v1**2 - Rational(19,3)*v1*v2 + Rational(4,3)*v2**2) + +def complete_the_square(poly, var): + """ + 对多项式poly相对于变量var进行配方。 + 返回:(平方项, 剩余多项式) + """ + # 提取系数:A (var^2), D (var^1), C (常数项) + A = poly.coeff(var, 2) + D = poly.coeff(var, 1) + C = poly - A * var**2 - D * var + + if A == 0: + return 0, poly # 无二次项 + + # 移位 delta = -D / (2 A) + delta = -D / (2 * A) + # 平方项 + square_part = A * (var + delta)**2 + # 剩余 + remaining = C - (D**2) / (4 * A) + + return simplify(square_part), simplify(remaining) + +# 应用:选择 var = v1 (v(i+1),系数最大) +var = v1 +square_part, remaining = complete_the_square(beta0, var) + +print("beta0 的 SOS 分解:") +print("平方项 1:") +pprint(square_part) +print("\n剩余(平方项 2):") +pprint(remaining) +print("\n完整形式:beta0 = 平方项1 + 剩余") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/04a/testprj.py b/example/figure/1d/weno/some_help_code/04a/testprj.py new file mode 100644 index 00000000..2f34fe9c --- /dev/null +++ b/example/figure/1d/weno/some_help_code/04a/testprj.py @@ -0,0 +1,58 @@ +from sympy import symbols, Rational, simplify, expand, latex, factor + +# 定义符号(v0 = v(i), v1 = v(i+1), v2 = v(i+2)) +v0, v1, v2 = symbols('v0 v1 v2') + +# beta0 的展开形式 +beta0 = (Rational(10,3)*v0**2 - Rational(31,3)*v0*v1 + Rational(11,3)*v0*v2 + + Rational(25,3)*v1**2 - Rational(19,3)*v1*v2 + Rational(4,3)*v2**2) + +def complete_the_square(poly, var): + """ + 对多项式 poly 相对于变量 var 进行配方。 + 返回:(平方项, 剩余多项式) + """ + A = poly.coeff(var, 2) + D = poly.coeff(var, 1) + C = poly - A * var**2 - D * var + + if A == 0: + return 0, poly + + # delta = D / (2 * A) + delta = D / (2 * A) + square_part = A * (var + delta)**2 + remaining = C - (D**2) / (4 * A) + + return simplify(square_part), simplify(remaining) + +# 应用:选择 var = v1 (系数最大) +var = v1 +square_part, remaining = complete_the_square(beta0, var) + +# 简化剩余为平方形式 +remaining_squared = factor(remaining) + +# 修正:SOS 使用简化平方 +sos = square_part + remaining_squared + +# 验证 +expanded_sos = expand(sos) +difference = simplify(beta0 - expanded_sos) +print("验证:展开 SOS 与原始差值 =", difference) # 应为 0 + +# LaTeX 输出 +print("\n原始公式 LaTeX:") +print(latex(beta0)) +print("\n平方项 LaTeX:") +print(latex(square_part)) +print("\n剩余项 LaTeX (简化平方):") +print(latex(remaining_squared)) +print("\n完整 SOS LaTeX:") +print(latex(sos)) + +# 目标形式验证 +target = Rational(13,12) * (v0 - 2*v1 + v2)**2 + Rational(1,4) * (3*v0 - 4*v1 + v2)**2 +target_expanded = expand(target) +target_diff = simplify(beta0 - target_expanded) +print("\n目标形式与原始差值 =", target_diff) # 应为 0 \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/04b/testprj.py b/example/figure/1d/weno/some_help_code/04b/testprj.py new file mode 100644 index 00000000..7730abbd --- /dev/null +++ b/example/figure/1d/weno/some_help_code/04b/testprj.py @@ -0,0 +1,97 @@ +from sympy import Rational +from itertools import product +from math import gcd as math_gcd # 更快整数 gcd + +# beta0 的对称矩阵(二次型 A,off-diag 是跨项系数的一半) +A_beta = [ + [Rational(10,3), Rational(-31,6), Rational(11,6)], + [Rational(-31,6), Rational(25,3), Rational(-19,6)], + [Rational(11,6), Rational(-19,6), Rational(4,3)] +] + +def gcd_list(lst): + """计算列表整数的最大公约数(处理 0)。""" + g = 0 + for x in lst: + g = math_gcd(g, abs(x)) + return g + +def normalize_vector(vec): + """标准化向量:gcd=1,第一非零系数正。""" + g = gcd_list(vec) + if g == 0: + return tuple(vec) + vec = tuple(int(x) // g for x in vec) + # 翻转符号使第一非零正 + for i in range(len(vec)): + if vec[i] != 0: + if vec[i] < 0: + vec = tuple(-int(x) for x in vec) + break + return vec + +def check_sos_2_terms(l1, l2, A): + """检查是否为有效 2 平方 SOS:解 lambda 并验证所有系数。""" + a, b, c = l1 + d, e, f = l2 + p = a**2 + q = d**2 + r = b**2 + s = e**2 + det = p * s - q * r + if det == 0: + return None + # Cramer 法则解 lambda1, lambda2 (基于 v0² 和 v1² 系数) + lambda1 = (A[0][0] * s - A[1][1] * q) / det + lambda2 = (A[1][1] * p - A[0][0] * r) / det + # 预测所有系数 + pred_00 = lambda1 * p + lambda2 * q + pred_11 = lambda1 * r + lambda2 * s + pred_22 = lambda1 * c**2 + lambda2 * f**2 + pred_01 = lambda1 * a * b + lambda2 * d * e + pred_02 = lambda1 * a * c + lambda2 * d * f + pred_12 = lambda1 * b * c + lambda2 * e * f + # 验证(对称矩阵,只查上三角) + if (pred_00 == A[0][0] and pred_11 == A[1][1] and pred_22 == A[2][2] and + pred_01 == A[0][1] and pred_02 == A[0][2] and pred_12 == A[1][2] and + lambda1 > 0 and lambda2 > 0): + return (l1, lambda1, l2, lambda2) + return None + +def find_all_sos_2(A, max_c=4): # 默认 max_c=4 以快速找到目标形式 + """枚举所有 2 平方 SOS,小系数版本。""" + print(f"Starting search with max_c={max_c}... Total iterations: {(2*max_c + 1)**6}") + sos_list = [] + processed = set() # 去重已标准化对 + total = (2 * max_c + 1) ** 6 + count = 0 + for coeffs in product(range(-max_c, max_c + 1), repeat=6): + count += 1 + if count % 50000 == 0: # 每 5 万迭代打印进度 + print(f"Progress: {count}/{total} ({count / total * 100:.1f}%) | Processed pairs: {len(processed)}") + l1_raw = coeffs[0:3] + l2_raw = coeffs[3:6] + if all(x == 0 for x in l1_raw) or all(x == 0 for x in l2_raw): # 跳过零向量 + continue + l1 = normalize_vector(l1_raw) + l2 = normalize_vector(l2_raw) + pair = tuple(sorted([l1, l2])) # 排序避免 (l1,l2) 和 (l2,l1) 重复 + if pair in processed: + continue + processed.add(pair) + res = check_sos_2_terms(l1, l2, A) + if res: + sos_list.append(res) + print(f"Found one: {res}") # 立即打印发现 + print(f"Search complete. Found {len(sos_list)} representations.") + return sos_list + +# 运行 2 平方搜索(从小 max_c 开始) +if __name__ == "__main__": + sos_2 = find_all_sos_2(A_beta, max_c=4) # 改成 6 如果想找更多 + print("\n=== 找到的 2 平方 SOS 表示(3 项线性形式 + 2 平方) ===") + for i, (l1, lam1, l2, lam2) in enumerate(sos_2, 1): + print(f"{i}. \\beta_0 = {lam1} ({l1[0]} v_0 + {l1[1]} v_1 + {l1[2]} v_2)^2 + {lam2} ({l2[0]} v_0 + {l2[1]} v_1 + {l2[2]} v_2)^2") + + print("\n注: 3 平方 SOS 可能无小系数严格正 lambda 表示(因秩 2),可通过添加第三个 0 lambda 退化到 2 平方。") + print("若需 3 平方扩展,增大 max_c 但计算量 O(max_c^9),建议 max_c=2。") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/04c/testprj.py b/example/figure/1d/weno/some_help_code/04c/testprj.py new file mode 100644 index 00000000..798282fd --- /dev/null +++ b/example/figure/1d/weno/some_help_code/04c/testprj.py @@ -0,0 +1,108 @@ +from sympy import Rational +from itertools import product +from math import gcd as math_gcd + +# beta0 的对称矩阵(二次型 A,off-diag 是跨项系数的一半) +A_beta = [ + [Rational(10,3), Rational(-31,6), Rational(11,6)], + [Rational(-31,6), Rational(25,3), Rational(-19,6)], + [Rational(11,6), Rational(-19,6), Rational(4,3)] +] + +def gcd_list(lst): + """计算列表整数的最大公约数(处理 0)。""" + g = 0 + for x in lst: + g = math_gcd(g, abs(x)) + return g + +def normalize_vector(vec): + """标准化向量:gcd=1,第一非零系数正(忽略零系数)。""" + non_zero = [x for x in vec if x != 0] + if not non_zero: + return tuple(vec) + g = gcd_list(non_zero) + vec_norm = tuple(int(x) // g if x != 0 else 0 for x in vec) + # 翻转符号使第一非零正 + for i in range(len(vec_norm)): + if vec_norm[i] != 0: + if vec_norm[i] < 0: + vec_norm = tuple(-int(x) if x != 0 else 0 for x in vec_norm) + break + return vec_norm + +def check_sos_2_terms(l1, l2, A): + """检查是否为有效 2 平方 SOS:解 lambda 并验证所有系数。""" + a, b, c = l1 + d, e, f = l2 + p = a**2 + q = d**2 + r = b**2 + s = e**2 + det = p * s - q * r + if det == 0: + return None + # Cramer 法则解 lambda1, lambda2 (基于 v0² 和 v1² 系数) + lambda1 = (A[0][0] * s - A[1][1] * q) / det + lambda2 = (A[1][1] * p - A[0][0] * r) / det + # 预测剩余系数 + pred_22 = lambda1 * c**2 + lambda2 * f**2 + pred_01 = lambda1 * a * b + lambda2 * d * e + pred_02 = lambda1 * a * c + lambda2 * d * f + pred_12 = lambda1 * b * c + lambda2 * e * f + # 验证(对称矩阵,只查上三角) + if (pred_22 == A[2][2] and pred_01 == A[0][1] and pred_02 == A[0][2] and pred_12 == A[1][2] and + lambda1 > 0 and lambda2 > 0): + return (l1, lambda1, l2, lambda2) + return None + +def find_all_sos_2(A, max_c=4): + """枚举所有 2 平方 SOS(包括稀疏支持集,如 3+2, 2+2)。""" + print(f"Starting search with max_c={max_c}... Total iterations: {(2*max_c + 1)**6}") + sos_list = [] + processed = set() # 去重已标准化对 + total = (2 * max_c + 1) ** 6 + count = 0 + for coeffs in product(range(-max_c, max_c + 1), repeat=6): + count += 1 + if count % 50000 == 0: # 每 5 万迭代打印进度 + print(f"Progress: {count}/{total} ({count / total * 100:.1f}%) | Processed pairs: {len(processed)}") + l1_raw = coeffs[0:3] + l2_raw = coeffs[3:6] + if all(x == 0 for x in l1_raw) or all(x == 0 for x in l2_raw): # 跳过零向量 + continue + l1 = normalize_vector(l1_raw) + l2 = normalize_vector(l2_raw) + pair = tuple(sorted([l1, l2])) # 排序避免 (l1,l2) 和 (l2,l1) 重复 + if pair in processed: + continue + processed.add(pair) + res = check_sos_2_terms(l1, l2, A) + if res: + sos_list.append(res) + print(f"Found one: {res}") # 立即打印发现 + print(f"Search complete. Found {len(sos_list)} representations.") + return sos_list + +def classify_supports(sos_list): + """分类找到的形式按支持集大小 (e.g., 3+3, 3+2)。""" + from collections import defaultdict + groups = defaultdict(list) + for l1, lam1, l2, lam2 in sos_list: + supp1 = sum(1 for x in l1 if x != 0) + supp2 = sum(1 for x in l2 if x != 0) + key = f"{max(supp1, supp2)}+{min(supp1, supp2)}" # 排序如 3+2 + groups[key].append((l1, lam1, l2, lam2)) + return groups + +# 运行搜索与分类 +if __name__ == "__main__": + sos_2 = find_all_sos_2(A_beta, max_c=4) + print("\n=== 所有找到的 2 平方 SOS 表示(按支持集分类) ===") + groups = classify_supports(sos_2) + for combo, forms in sorted(groups.items()): + print(f"\n{combo} 组合 ({len(forms)} 个):") + for i, (l1, lam1, l2, lam2) in enumerate(forms, 1): + print(f" {i}. \\beta_0 = {lam1} ({l1[0]} v_0 + {l1[1]} v_1 + {l1[2]} v_2)^2 + {lam2} ({l2[0]} v_0 + {l2[1]} v_1 + {l2[2]} v_2)^2") + + print("\n注: 无找到 3+2 或 2+2(max_c=4 下);增大 max_c=6 可能发现更多稀疏形式,但计算 ~10x 更长。") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/04d/testprj.py b/example/figure/1d/weno/some_help_code/04d/testprj.py new file mode 100644 index 00000000..2a5113d6 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/04d/testprj.py @@ -0,0 +1,119 @@ +from sympy import Rational +from itertools import product +from math import gcd as math_gcd +from collections import defaultdict # 添加以防 + +# 修正矩阵(off-diagonal 非 0) +A_beta0 = [ + [Rational(10,3), Rational(-31,6), Rational(11,6)], + [Rational(-31,6), Rational(25,3), Rational(-19,6)], + [Rational(11,6), Rational(-19,6), Rational(4,3)] +] + +A_beta1 = [ + [Rational(4,3), Rational(-13,6), Rational(5,6)], + [Rational(-13,6), Rational(13,3), Rational(-13,6)], + [Rational(5,6), Rational(-13,6), Rational(4,3)] +] + +A_beta2 = [ + [Rational(4,3), Rational(-19,6), Rational(11,6)], + [Rational(-19,6), Rational(25,3), Rational(-31,6)], + [Rational(11,6), Rational(-31,6), Rational(10,3)] +] + +ALL_BETAS = {'beta0': A_beta0, 'beta1': A_beta1, 'beta2': A_beta2} + +def gcd_list(lst): + g = 0 + for x in lst: + g = math_gcd(g, abs(x)) + return g + +def normalize_vector(vec): + non_zero = [x for x in vec if x != 0] + if not non_zero: + return tuple(vec) + g = gcd_list(non_zero) + vec_norm = tuple(int(x) // g if x != 0 else 0 for x in vec) + for i in range(len(vec_norm)): + if vec_norm[i] != 0: + if vec_norm[i] < 0: + vec_norm = tuple(-int(x) if x != 0 else 0 for x in vec_norm) + break + return vec_norm + +def check_sos_2_terms(l1, l2, A): + a, b, c = l1 + d, e, f = l2 + p = a**2 + q = d**2 + r = b**2 + s = e**2 + det = p * s - q * r + if det == 0: + return None + lambda1 = (A[0][0] * s - A[1][1] * q) / det + lambda2 = (A[1][1] * p - A[0][0] * r) / det + pred_22 = lambda1 * c**2 + lambda2 * f**2 + pred_01 = lambda1 * a * b + lambda2 * d * e + pred_02 = lambda1 * a * c + lambda2 * d * f + pred_12 = lambda1 * b * c + lambda2 * e * f + if (pred_22 == A[2][2] and pred_01 == A[0][1] and pred_02 == A[0][2] and pred_12 == A[1][2] and + lambda1 > 0 and lambda2 > 0): + return (l1, lambda1, l2, lambda2) + return None + +def find_all_sos_2(A, max_c=4, name='beta'): + print(f"\n--- Searching for {name} with max_c={max_c}... ---") + print(f"Total iterations: {(2*max_c + 1)**6}") + sos_list = [] + processed = set() + total = (2 * max_c + 1) ** 6 + count = 0 + for coeffs in product(range(-max_c, max_c + 1), repeat=6): + count += 1 + if count % 50000 == 0: + print(f"Progress: {count}/{total} ({count / total * 100:.1f}%) | Processed pairs: {len(processed)}") + l1_raw = coeffs[0:3] + l2_raw = coeffs[3:6] + if all(x == 0 for x in l1_raw) or all(x == 0 for x in l2_raw): + continue + l1 = normalize_vector(l1_raw) + l2 = normalize_vector(l2_raw) + pair = tuple(sorted([l1, l2])) + if pair in processed: + continue + processed.add(pair) + res = check_sos_2_terms(l1, l2, A) + if res: + sos_list.append(res) + print(f"Found one for {name}: {res}") + print(f"Search for {name} complete. Found {len(sos_list)} representations.") + return sos_list + +def classify_supports(sos_list): + groups = defaultdict(list) + for l1, lam1, l2, lam2 in sos_list: + supp1 = sum(1 for x in l1 if x != 0) + supp2 = sum(1 for x in l2 if x != 0) + key = f"{max(supp1, supp2)}+{min(supp1, supp2)}" + groups[key].append((l1, lam1, l2, lam2)) + return groups + +if __name__ == "__main__": + max_c = 4 # 可增大到 6 + all_results = {} + for name, A in ALL_BETAS.items(): + sos = find_all_sos_2(A, max_c, name) + all_results[name] = classify_supports(sos) + + print("\n=== 所有 β 的 SOS 表示(按支持集分类) ===") + for name, groups in all_results.items(): + print(f"\n{name.upper()}:") + for combo, forms in sorted(groups.items()): + print(f" {combo} 组合 ({len(forms)} 个):") + for i, (l1, lam1, l2, lam2) in enumerate(forms, 1): + print(f" {i}. \\beta_{name} = {lam1} ({l1[0]} v_0 + {l1[1]} v_1 + {l1[2]} v_2)^2 + {lam2} ({l2[0]} v_0 + {l2[1]} v_1 + {l2[2]} v_2)^2") + + print(f"\n注: max_c={max_c} 下结果;若无稀疏形式,增大 max_c。总运行时间 ~2min。") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/05/testprj.py b/example/figure/1d/weno/some_help_code/05/testprj.py new file mode 100644 index 00000000..0fb19a71 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/05/testprj.py @@ -0,0 +1,106 @@ +import sympy as sp + +# 定义符号 +i = sp.symbols('i', integer=True) +v = sp.Function('v') +# 定义四个连续点的函数值 +v_i = v(i) +v_ip1 = v(i + 1) +v_ip2 = v(i + 2) +v_ip3 = v(i + 3) + +# 定义展开式(按照给定的系数) +beta0_expanded = ( + sp.Rational(2107, 240) * v_i**2 + - sp.Rational(1567, 40) * v_i * v_ip1 + + sp.Rational(3521, 120) * v_i * v_ip2 + - sp.Rational(309, 40) * v_i * v_ip3 + + sp.Rational(11003, 240) * v_ip1**2 + - sp.Rational(8623, 120) * v_ip1 * v_ip2 + + sp.Rational(2321, 120) * v_ip1 * v_ip3 + + sp.Rational(7043, 240) * v_ip2**2 + - sp.Rational(647, 40) * v_ip2 * v_ip3 + + sp.Rational(547, 240) * v_ip3**2 +) + +# 变量列表(为了方便提取系数) +vars_list = [v_i, v_ip1, v_ip2, v_ip3] + +def complete_the_square(poly, var, vars_list): + """ + 对多项式 poly 相对于变量 var 进行配方。 + 返回:(平方项, 剩余多项式) + """ + # 提取 A (var^2 系数), D (var^1 系数), C (无 var 项) + A = poly.coeff(var, 2) + D = poly.coeff(var, 1) + C = poly.as_poly(vars_list).as_expr() - A * var**2 - D * var # 剩余部分(简化版) + + if A == 0: + return 0, poly + + # delta = D / (2 * A) # 注意:标准配方是 -D/(2A),但根据二次形式 A x^2 + B x + C,delta = -B/(2A) + # 修正:D 是 B,这里 delta = -D / (2*A) + delta = -D / (2 * A) + square_part = A * (var + delta)**2 + remaining = C - D**2 / (4 * A) + + return sp.simplify(square_part), sp.simplify(remaining) + +def successive_completion(poly, vars_list, max_steps=10): + """ + 逐次配方法:反复配方直到剩余为常数或简单形式。 + 选择当前二次系数绝对值最大的变量配方。 + 返回:SOS 列表(平方项们) + """ + current_poly = poly + sos_terms = [] + steps = 0 + + while steps < max_steps: + # 找当前二次系数最大的变量 + max_coeff = 0 + best_var = None + for var in vars_list: + coeff = current_poly.coeff(var, 2) + if abs(coeff) > max_coeff: + max_coeff = abs(coeff) + best_var = var + + if max_coeff == 0: + # 无二次项,剩余为线性/常数(理想 SOS 为 0) + break + + # 配方 + square_part, remaining = complete_the_square(current_poly, best_var, vars_list) + sos_terms.append(square_part) + current_poly = remaining + steps += 1 + print(f"Step {steps}: Distributed {best_var}, square: {square_part}, remaining degree: {sp.degree(current_poly)}") + + if current_poly != 0: + sos_terms.append(current_poly) # 剩余作为最后一项(可能需进一步因子) + + return sos_terms + +# 运行逐次配方法 +print("Original beta0_expanded:") +sp.pprint(beta0_expanded) +print("\nDegree:", sp.degree(beta0_expanded)) + +sos_terms = successive_completion(beta0_expanded, vars_list) + +print("\n=== SOS 分解(逐次配方) ===") +sos = sum(sos_terms) +sp.pprint(sos) +print("\n完整 SOS 形式:") +sp.pprint(sp.expand(sos)) + +# 验证:展开 SOS 与原始差值 +difference = sp.simplify(sp.expand(sos) - beta0_expanded) +print("\n验证差值(应为 0):", difference) + +# 如果剩余复杂,可尝试因子化或手动调整 +for term in sos_terms: + print("\nTerm:") + sp.pprint(sp.factor(term)) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/05a/testprj.py b/example/figure/1d/weno/some_help_code/05a/testprj.py new file mode 100644 index 00000000..4ff8ebf8 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/05a/testprj.py @@ -0,0 +1,76 @@ +import sympy as sp + +# 作为符号处理(避免 Function 问题) +v_i, v_ip1, v_ip2, v_ip3 = sp.symbols('v_i v_{i+1} v_{i+2} v_{i+3}') + +beta0_expanded = ( + sp.Rational(2107, 240) * v_i**2 - sp.Rational(1567, 40) * v_i * v_ip1 + sp.Rational(3521, 120) * v_i * v_ip2 - sp.Rational(309, 40) * v_i * v_ip3 + + sp.Rational(11003, 240) * v_ip1**2 - sp.Rational(8623, 120) * v_ip1 * v_ip2 + sp.Rational(2321, 120) * v_ip1 * v_ip3 + + sp.Rational(7043, 240) * v_ip2**2 - sp.Rational(647, 40) * v_ip2 * v_ip3 + sp.Rational(547, 240) * v_ip3**2 +) + +def complete_the_square(poly, var): + poly_exp = sp.expand(poly) + A = poly_exp.coeff(var, 2) + B = poly_exp.coeff(var, 1) + C = poly_exp - A * var**2 - B * var + + if A == 0: + return 0, poly + + delta = -B / (2 * A) + square_part = A * (var + delta)**2 + remaining = C - B**2 / (4 * A) + + return sp.simplify(square_part), sp.simplify(remaining) + +def successive_completion(poly, order): + current_poly = poly + sos_terms = [] + + for var in order: + A = sp.expand(current_poly).coeff(var, 2) + if A == 0: + break + square_part, remaining = complete_the_square(current_poly, var) + sos_terms.append(square_part) + current_poly = remaining + + if current_poly != 0: + sos_terms.append(current_poly) + + return sos_terms + +# 尝试顺序直到 diff = 0 +orders = [ + [v_ip1, v_ip2, v_i, v_ip3], + [v_ip2, v_ip1, v_i, v_ip3], + [v_ip2, v_ip1, v_ip3, v_i], + [v_ip1, v_ip3, v_ip2, v_i] +] + +best_sos = None +for idx, order in enumerate(orders, 1): + print(f"--- Order {idx}: {order} ---") + sos_terms = successive_completion(beta0_expanded, order) + sos = sum(sos_terms) + diff = sp.simplify(sp.expand(sos) - beta0_expanded) + print("Diff:", diff) + if diff == 0: + best_sos = sos + print("Success! LaTeX SOS:") + print(sp.latex(best_sos)) + break + +if best_sos is None: + print("No exact SOS found with these orders. Try larger steps or SDP.") + +# PSD 检查 +A = sp.Matrix([ + [sp.Rational(2107,240), sp.Rational(-1567,80), sp.Rational(3521,240), sp.Rational(-309,80)], + [sp.Rational(-1567,80), sp.Rational(11003,240), sp.Rational(-8623,240), sp.Rational(2321,240)], + [sp.Rational(3521,240), sp.Rational(-8623,240), sp.Rational(7043,240), sp.Rational(-647,80)], + [sp.Rational(-309,80), sp.Rational(2321,240), sp.Rational(-647,80), sp.Rational(547,240)] +]) +eigs_num = [float(e) for e in A.applyfunc(sp.N).eigenvals().values()] +print("Numerical eigenvalues (all >=0 for PSD):", eigs_num) \ No newline at end of file diff --git a/example/figure/1d/weno/wenoinfo/01/wenoinfo.py b/example/figure/1d/weno/wenoinfo/01/wenoinfo.py new file mode 100644 index 00000000..2f4a50cd --- /dev/null +++ b/example/figure/1d/weno/wenoinfo/01/wenoinfo.py @@ -0,0 +1,1016 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + #print(f"sum_integrals_same_bounds polynomials={polynomials}") + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + #print(f" Integration Result for Term{idx+1}: {format_expression(integral_result)}") + print(f" Integration Result for Term{idx+1}: {format_expression_fraction(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def format_expression_fraction(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + max_denominator = 1000000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + if len(symbols) == 1: + term_strs.append(f"{frac_str}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{frac_str}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{frac_str}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def build_moment_matrix(r: int, k: int) -> np.ndarray: + rows = [] + for m in range(k): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -r + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point(k,r,x_point): + #M = build_moment_matrix(k,r) + M = compute_mass_matrix(k,r) + #print(f'M={M}') + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv={M_inv}') + monomials = np.array([x_point ** i for i in range(k)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def generate_weno_substencils(k: int, x_point: Fraction) -> np.ndarray: + stencils = [] + for r in range(k): + # r = 0 → rightmost stencil + # r = k-1 → leftmost stencil + coef = compute_stencil_coefficients_for_point(k,r,x_point) + #print(f'coef={coef}') + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(k: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(k, offset) + +def generate_right_stencils(k: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(k, -offset) + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\nInitial Polynomial Expressions (before integration):") + for i, poly in enumerate(squared_polynomials, 1): + print(f" Polynomial Term {i}: P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def to_fraction(num, max_denominator=1000000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + return frac + +def float_to_fraction_str(num, max_denominator=1000000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}·" + frac_star = f"{frac_str}" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f"k={k},polynomial_list={polynomial_list}") + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + lower_bound = -0.5 + upper_bound = 0.5 + domain = f"[{to_fraction(lower_bound)}, {to_fraction(upper_bound)}]" + print(f"\nStep-by-step Integration and Summation (integration domain: x∈{domain}):") + + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + #print(f"k={k},total_result={total_result}") + + # 打印最终结果 + print(f"\nFinal Aggregated Result (sum of all integrated terms):") + #formatted_result = format_expression(total_result) + formatted_result = format_expression_fraction(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def sort_by_first_list(primary_list, *other_lists, key=None, reverse=False): + """ + 根据第一个列表排序,同步调整任意数量其他列表 + + 参数: + primary_list: 主排序参考列表 + *other_lists: 其他需要同步排序的列表(可变参数) + key: 排序key函数(如abs, lambda x: x**2等) + reverse: 是否降序 + + 返回: + 元组: (sorted_primary, sorted_other1, sorted_other2, ...) + """ + # 核心:动态生成key函数 + if key is None: + key_func = lambda i: primary_list[i] # 默认:直接比较值 + else: + key_func = lambda i: key(primary_list[i]) # 自定义:对值应用key函数 + + # 获取排序索引 + indices = sorted(range(len(primary_list)), key=key_func, reverse=reverse) + + # 应用索引到所有列表(包括主列表) + all_lists = (primary_list,) + other_lists + result = tuple([lst[i] for i in indices] for lst in all_lists) + return result + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + frac_list = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000000) + frac_list.append(frac) + #term_strs.append(f"{frac}·{symbol_str_final}") + term_strs.append(f"{frac}{symbol_str_final}") + + _, term_strs = sort_by_first_list(frac_list, term_strs, key=abs, reverse=True) + + return " + ".join(term_strs) + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def print_smoothness_indicators(expression,coeffs_list,k): + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + for r in range(k): + a_coeffs = coeffs_list[r] + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicatorOld(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + #print(f"k={k},a_coeffs={a_coeffs}") + coeffs_list.append( a_coeffs ) + + print_smoothness_indicators(total_result,coeffs_list,k) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + #demo_smoothness_indicator(1) + #demo_smoothness_indicator(2) + #demo_smoothness_indicator(3) + kk = 3 + matrix_stencils = generate_left_stencils(kk) + print_matrix_fraction(matrix_stencils) + demo_smoothness_indicator(kk) \ No newline at end of file diff --git a/example/weno-coef/crj/python/01e/crj.py b/example/weno-coef/crj/python/01e/crj.py new file mode 100644 index 00000000..0a6fd918 --- /dev/null +++ b/example/weno-coef/crj/python/01e/crj.py @@ -0,0 +1,48 @@ +from fractions import Fraction + +def calculate_crj(r, j, k): + result = Fraction(0, 1) + for m in range(j + 1, k + 1): + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + result += Fraction(numerator, denominator) + return result + +for k in range(1,8): + print(f"=== k = {k} ===") + mat = [] + r_values = range(-1, k) + for r in r_values: + row = [] + for j in range(k): + row.append(calculate_crj(r, j, k)) + mat.append(row) + + # 计算宽度(自动处理分数/负数长度) + max_width = max(len(str(item)) for row in mat for item in row) + r_width = max(len(str(r)) for r in r_values) + r_width = max(r_width, len("r")) # 表头“r”也占位 + + # 打印表头 + header = " ".join(f"{j:^{max_width}}" for j in range(k)) + print(f"{'r':>{r_width}} | {header}") + + # 打印数据行 + for r, row in zip(r_values, mat): + r_str = f"{r:>{r_width}}" + cells = " ".join(f"{str(item):^{max_width}}" for item in row) + print(f"{r_str} | {cells}") + print() # 每个 k 后空一行分隔 \ No newline at end of file diff --git a/example/weno-coef/crj/python/01f/crj.py b/example/weno-coef/crj/python/01f/crj.py new file mode 100644 index 00000000..342260e7 --- /dev/null +++ b/example/weno-coef/crj/python/01f/crj.py @@ -0,0 +1,52 @@ +from fractions import Fraction + +def calculate_crj(r, j, k): + result = Fraction(0, 1) + for m in range(j + 1, k + 1): + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + result += Fraction(numerator, denominator) + return result + +for k in range(1, 8): + print(f"=== k = {k} ===") + mat = [] + r_values = range(-1, k) + for r in r_values: + row = [] + for j in range(k): + row.append(calculate_crj(r, j, k)) + mat.append(row) + + # 所有会出现字符串(包括表头 j=0, j=1...)用于计算统一宽度 + header_cells = [f"j={j}" for j in range(k)] + all_strings = header_cells + [str(item) for row in mat for item in row] + max_width = max(len(s) for s in all_strings) if all_strings else 8 + + # r 列宽度(包含表头 "r") + r_strings = ["r"] + [str(r) for r in r_values] + r_width = max(len(s) for s in r_strings) + + # 表头 + header = " ".join(f"{cell:^{max_width}}" for cell in header_cells) + print(f"{'r':>{r_width}} | {header}") + + # 数据行(数值右对齐) + for r, row in zip(r_values, mat): + r_str = f"{r:>{r_width}}" + cells = " ".join(f"{str(item):>{max_width}}" for item in row) + print(f"{r_str} | {cells}") + print() \ No newline at end of file diff --git a/example/weno-coef/crj/python/01g/crj.py b/example/weno-coef/crj/python/01g/crj.py new file mode 100644 index 00000000..16d37ec9 --- /dev/null +++ b/example/weno-coef/crj/python/01g/crj.py @@ -0,0 +1,53 @@ +from fractions import Fraction + +def calculate_crj(r, j, k): + result = Fraction(0, 1) + for m in range(j + 1, k + 1): + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + result += Fraction(numerator, denominator) + return result + +for k in range(1, 8): + print(f"=== k = {k} ===") + mat = [] + # 关键修改:将 range(-1, k) 改为反向遍历(从大到小) + r_values = range(k - 1, -2, -1) # 原范围是 [-1, 0, 1, ..., k-1],反向后为 [k-1, ..., 1, 0, -1] + for r in r_values: + row = [] + for j in range(k): + row.append(calculate_crj(r, j, k)) + mat.append(row) + + # 所有会出现字符串(包括表头 j=0, j=1...)用于计算统一宽度 + header_cells = [f"j={j}" for j in range(k)] + all_strings = header_cells + [str(item) for row in mat for item in row] + max_width = max(len(s) for s in all_strings) if all_strings else 8 + + # r 列宽度(包含表头 "r") + r_strings = ["r"] + [str(r) for r in r_values] + r_width = max(len(s) for s in r_strings) + + # 表头 + header = " ".join(f"{cell:^{max_width}}" for cell in header_cells) + print(f"{'r':>{r_width}} | {header}") + + # 数据行(数值右对齐) + for r, row in zip(r_values, mat): + r_str = f"{r:>{r_width}}" + cells = " ".join(f"{str(item):>{max_width}}" for item in row) + print(f"{r_str} | {cells}") + print() \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01/testprj.py b/example/weno-coef/crj/python/expand_formula/01/testprj.py new file mode 100644 index 00000000..91654843 --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01/testprj.py @@ -0,0 +1,44 @@ +def expand_formula(k: int, r: int) -> str: + """ + 展开公式 v_{i+1/2} = \sum_{j=0}^{k-1} c_{rj} \bar{v}_{i-r+j} + 返回展开后的 LaTeX 字符串 + """ + terms = [] + for j in range(k): + # 系数 + coeff = f"c_{{{r}{j}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串(无空格,标准写法 i-2、i、i+3 等) + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset 为负时自动带 - + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 整项 + term = coeff + v_term + terms.append(term) + + # 左侧(保持你原始写法,也可以用 v_{i+1/2},如果你想要分数形式可改为 i + \frac{1}{2}) + left = r"v_{i+1/2}" + + # 用 " + " 连接所有项(自动处理只有一项或首项为负的情况) + equation = left + r" = " + " + ".join(terms) + + return equation + + +# 示例 +print("k=3, r=0 的展开结果:") +print(expand_formula(3, 0)) +print("\nk=3, r=1 的展开结果:") +print(expand_formula(3, 1)) +print("\nk=4, r=2 的展开结果:") +print(expand_formula(4, 2)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01a/testprj.py b/example/weno-coef/crj/python/expand_formula/01a/testprj.py new file mode 100644 index 00000000..981bea1a --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01a/testprj.py @@ -0,0 +1,44 @@ +def expand_formula(k: int, r: int) -> str: + """ + 展开公式 v_{i+1/2} = \sum_{j=0}^{k-1} c_{rj} \bar{v}_{i-r+j} + 返回展开后的 LaTeX 字符串 + """ + terms = [] + for j in range(k): + # 系数:改为 c_{r,j} 形式(添加逗号) + coeff = f"c_{{{r},{j}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串(无空格,标准写法 i-2、i、i+3 等) + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset 为负时自动带 - + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 整项:在系数和变量之间加薄空格 \, + term = coeff + r"\," + v_term + terms.append(term) + + # 左侧:改为 v_{i+1/2}^{(r)} 形式,其中 r 用真实值替换 + left = f"v_{{i+1/2}}^{{({r})}}" + + # 用 " + " 连接所有项(自动处理只有一项或首项为负的情况) + equation = left + r" = " + " + ".join(terms) + + return equation + + +# 示例 +print("k=3, r=0 的展开结果:") +print(expand_formula(3, 0)) +print("\nk=3, r=1 的展开结果:") +print(expand_formula(3, 1)) +print("\nk=4, r=2 的展开结果:") +print(expand_formula(4, 2)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01b/testprj.py b/example/weno-coef/crj/python/expand_formula/01b/testprj.py new file mode 100644 index 00000000..9f44ba86 --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01b/testprj.py @@ -0,0 +1,141 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的 r 参数 + j: 求和索引 j + k: 模板阶数 k + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m 从 j+1 到 k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def expand_formula_with_values(k: int, r: int) -> str: + """ + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的 r 参数 + + 返回: + str: 展开后的 LaTeX 字符串 + """ + terms = [] + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为 0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 将 Fraction 转换为 LaTeX 格式 + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + # 系数为 ±1,省略数字 + coeff_str = "" + else: + coeff_str = str(abs_fraction.numerator) + else: + # 分数情况 + coeff_str = f"\\frac{{{abs_fraction.numerator}}}{{{abs_fraction.denominator}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串 + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset 为负时自动带 - + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串 + if coeff_str == "": + # 系数为 ±1 + if is_negative: + term = "-" + v_term + else: + term = v_term + else: + # 系数不为 ±1 + term = coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是 0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + if i == 0: + # 第一项直接添加 + formatted_terms.append(term) + else: + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + # 组合方程 + equation = left + r" = " + right_side + + return equation + +# 示例 +if __name__ == "__main__": + print("k=3, r=0 的展开结果:") + print(expand_formula_with_values(3, 0)) + print("\nk=3, r=1 的展开结果:") + print(expand_formula_with_values(3, 1)) + print("\nk=4, r=2 的展开结果:") + print(expand_formula_with_values(4, 2)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01b0/testprj.py b/example/weno-coef/crj/python/expand_formula/01b0/testprj.py new file mode 100644 index 00000000..65f0278d --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01b0/testprj.py @@ -0,0 +1,166 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m从j+1到k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def expand_formula_with_values(k: int, r: int) -> str: + r""" + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的r参数 + + 返回: + str: 展开后的LaTeX字符串 + """ + terms = [] + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 将Fraction转换为LaTeX格式 + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + # 系数为±1,省略数字 + coeff_str = "" + else: + coeff_str = str(abs_fraction.numerator) + else: + # 分数情况 + coeff_str = f"\\frac{{{abs_fraction.numerator}}}{{{abs_fraction.denominator}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串 + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset为负时自动带- + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串(修正符号处理) + sign_prefix = "-" if is_negative else "" + + if coeff_str == "": + term = sign_prefix + v_term + else: + term = sign_prefix + coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + if i == 0: + # 第一项直接添加 + formatted_terms.append(term) + else: + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号,添加分隔符 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + return f"{left} = {right_side}" + +def generate_latex_array(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的array环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX array字符串 + """ + equations = [] + + # r从k-1到-1(降序) + for r in range(k-1, -2, -1): + eq = expand_formula_with_values(k, r) + equations.append(eq) + + # 构建array环境,左对齐 + array_content = " \\\\\\\\\n".join([f" {eq}" for eq in equations]) + return f"\\begin{{array}}{{l}}\n{array_content}\n\\end{{array}}" + +# 主函数示例 +if __name__ == "__main__": + # 测试单个展开 + print("单个展开测试:") + print("k=3, r=0 的展开结果:") + print(expand_formula_with_values(3, 0)) + print("\nk=3, r=1 的展开结果:") + print(expand_formula_with_values(3, 1)) + print("\nk=4, r=2 的展开结果:") + print(expand_formula_with_values(4, 2)) + print("\n" + "="*60 + "\n") + + # 测试array输出 + for k in [3, 4, 5]: + print(f"k={k} 时的展开公式组:") + print(generate_latex_array(k)) + print("\n" + "="*60 + "\n") \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01c/testprj.py b/example/weno-coef/crj/python/expand_formula/01c/testprj.py new file mode 100644 index 00000000..806999ca --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01c/testprj.py @@ -0,0 +1,180 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m从j+1到k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def expand_formula_with_values(k: int, r: int) -> str: + r""" + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的r参数 + + 返回: + str: 展开后的LaTeX字符串 + """ + terms = [] + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 将Fraction转换为LaTeX格式 + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + # 系数为±1,省略数字 + coeff_str = "" + else: + coeff_str = str(abs_fraction.numerator) + else: + # 分数情况 + coeff_str = f"\\frac{{{abs_fraction.numerator}}}{{{abs_fraction.denominator}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串 + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset为负时自动带- + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串(修正符号处理) + sign_prefix = "-" if is_negative else "" + + if coeff_str == "": + term = sign_prefix + v_term + else: + term = sign_prefix + coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + if i == 0: + # 第一项直接添加 + formatted_terms.append(term) + else: + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号,添加分隔符 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + return f"{left} = {right_side}" + +def generate_latex_array(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的array环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX array字符串 + """ + equations = [] + + # r从k-1到-1(降序) + for r in range(k-1, -2, -1): + eq = expand_formula_with_values(k, r) + equations.append(eq) + + # 构建array环境,左对齐 + array_content = " \\\\\\\\\n".join([f" {eq}" for eq in equations]) + return f"\\begin{{array}}{{l}}\n{array_content}\n\\end{{array}}" + +# 主函数示例 +if __name__ == "__main__": + # 示例:k=3 时的多行公式输出 + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_array(3)) + + print("\n" + "="*60 + "\n") + + # 示例:k=4 时的多行公式输出 + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_array(4)) + + print("\n" + "="*60 + "\n") + + # 示例:k=5 时的多行公式输出 + print("="*60) + print("k=5 时的展开公式组(r从4到-1):") + print("="*60) + print(generate_latex_array(5)) + + print("\n" + "="*60 + "\n") + + # 额外:测试k=2 + print("="*60) + print("k=2 时的展开公式组(r从1到-1):") + print("="*60) + print(generate_latex_array(2)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01d/testprj.py b/example/weno-coef/crj/python/expand_formula/01d/testprj.py new file mode 100644 index 00000000..a053e6dc --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01d/testprj.py @@ -0,0 +1,173 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m从j+1到k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def expand_formula_with_values(k: int, r: int) -> str: + r""" + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的r参数 + + 返回: + str: 展开后的LaTeX字符串 + """ + terms = [] + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 将Fraction转换为LaTeX格式 + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + # 系数为±1,省略数字 + coeff_str = "" + else: + coeff_str = str(abs_fraction.numerator) + else: + # 分数情况 + coeff_str = f"\\frac{{{abs_fraction.numerator}}}{{{abs_fraction.denominator}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串 + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset为负时自动带- + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串(修正符号处理) + sign_prefix = "-" if is_negative else "" + + if coeff_str == "": + term = sign_prefix + v_term + else: + term = sign_prefix + coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + if i == 0: + # 第一项直接添加 + formatted_terms.append(term) + else: + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号,添加分隔符 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + return f"{left} = {right_side}" + +def generate_latex_array(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的array环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX array字符串 + """ + equations = [] + + # r从k-1到-1(降序) + for r in range(k-1, -2, -1): + eq = expand_formula_with_values(k, r) + equations.append(eq) + + # 构建array环境,左对齐 + # 修正:使用正确的双反斜杠换行符(LaTeX中的\\对应Python字符串中的\\\\) + array_content = " \\\\\n".join([f" {eq}" for eq in equations]) + return f"\\begin{{array}}{{l}}\n{array_content}\n\\end{{array}}" + +# 主函数示例 +if __name__ == "__main__": + # 示例:k=3 时的多行公式输出 + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_array(3)) + + print("\n" + "="*60 + "\n") + + # 示例:k=4 时的多行公式输出 + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_array(4)) + + print("\n" + "="*60 + "\n") + + # 示例:k=5 时的多行公式输出 + print("="*60) + print("k=5 时的展开公式组(r从4到-1):") + print("="*60) + print(generate_latex_array(5)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01e/testprj.py b/example/weno-coef/crj/python/expand_formula/01e/testprj.py new file mode 100644 index 00000000..ebdcfd5b --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01e/testprj.py @@ -0,0 +1,175 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m从j+1到k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def expand_formula_with_values(k: int, r: int) -> str: + r""" + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的r参数 + + 返回: + str: 展开后的LaTeX字符串 + """ + terms = [] + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 将Fraction转换为LaTeX格式 + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + # 系数为±1,省略数字 + coeff_str = "" + else: + coeff_str = str(abs_fraction.numerator) + else: + # 分数情况 + coeff_str = f"\\frac{{{abs_fraction.numerator}}}{{{abs_fraction.denominator}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串(使用\phantom实现宽度对齐) + # i -> i\phantom{+0} (占位但不显示,宽度与i+1相同) + # i+1 -> i+1 + # i-1 -> i-1 + if offset == 0: + subscript = r"i\hphantom{+0}" # 关键修改:占位对齐 + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset为负时自动带- + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串(修正符号处理) + sign_prefix = "-" if is_negative else "" + + if coeff_str == "": + term = sign_prefix + v_term + else: + term = sign_prefix + coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + if i == 0: + # 第一项直接添加 + formatted_terms.append(term) + else: + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号,添加分隔符 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + return f"{left} = {right_side}" + +def generate_latex_array(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的array环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX array字符串 + """ + equations = [] + + # r从k-1到-1(降序) + for r in range(k-1, -2, -1): + eq = expand_formula_with_values(k, r) + equations.append(eq) + + # 构建array环境,左对齐 + array_content = " \\\\\n".join([f" {eq}" for eq in equations]) + return f"\\begin{{array}}{{l}}\n{array_content}\n\\end{{array}}" + +# 主函数示例 +if __name__ == "__main__": + # 示例:k=3 时的多行公式输出 + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_array(3)) + + print("\n" + "="*60 + "\n") + + # 示例:k=4 时的多行公式输出 + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_array(4)) + + print("\n" + "="*60 + "\n") + + # 示例:k=5 时的多行公式输出 + print("="*60) + print("k=5 时的展开公式组(r从4到-1):") + print("="*60) + print(generate_latex_array(5)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01f/testprj.py b/example/weno-coef/crj/python/expand_formula/01f/testprj.py new file mode 100644 index 00000000..cfe46149 --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01f/testprj.py @@ -0,0 +1,171 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m从j+1到k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def expand_formula_with_values(k: int, r: int) -> str: + r""" + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的r参数 + + 返回: + str: 展开后的LaTeX字符串 + """ + terms = [] + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 将Fraction转换为LaTeX格式 + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + # 系数为±1,省略数字 + coeff_str = "" + else: + coeff_str = str(abs_fraction.numerator) + else: + # 分数情况 + coeff_str = f"\\frac{{{abs_fraction.numerator}}}{{{abs_fraction.denominator}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串(使用\phantom实现宽度对齐) + # i -> i\phantom{+0} (占位但不显示,宽度与i+1相同) + # i+1 -> i+1 + # i-1 -> i-1 + if offset == 0: + subscript = r"i\hphantom{+0}" # 关键修改:占位对齐 + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset为负时自动带- + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串(修正符号处理) + sign_prefix = "-" if is_negative else "" + + if coeff_str == "": + term = sign_prefix + v_term + else: + term = sign_prefix + coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号,添加分隔符 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + return f"{left} = {right_side}" + +def generate_latex_array(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的array环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX array字符串 + """ + equations = [] + + # r从k-1到-1(降序) + for r in range(k-1, -2, -1): + eq = expand_formula_with_values(k, r) + equations.append(eq) + + # 构建array环境,左对齐 + array_content = " \\\\\n".join([f" {eq}" for eq in equations]) + return f"\\begin{{array}}{{l}}\n{array_content}\n\\end{{array}}" + +# 主函数示例 +if __name__ == "__main__": + # 示例:k=3 时的多行公式输出 + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_array(3)) + + print("\n" + "="*60 + "\n") + + # 示例:k=4 时的多行公式输出 + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_array(4)) + + print("\n" + "="*60 + "\n") + + # 示例:k=5 时的多行公式输出 + print("="*60) + print("k=5 时的展开公式组(r从4到-1):") + print("="*60) + print(generate_latex_array(5)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01f0/testprj.py b/example/weno-coef/crj/python/expand_formula/01f0/testprj.py new file mode 100644 index 00000000..bde9b35e --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01f0/testprj.py @@ -0,0 +1,147 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + for m in range(j + 1, k + 1): + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + result += Fraction(numerator, denominator) + + return result + +def calculate_max_widths(k: int) -> dict: + """ + 计算所有公式中最大的分子、分母宽度 + + 参数: + k: 模板阶数 + + 返回: + dict: {'num': 最大分子长度, 'den': 最大分母长度} + """ + max_num_len = 0 + max_den_len = 0 + + for r in range(k-1, -2, -1): + for j in range(k): + coeff = calculate_crj(r, j, k) + if coeff == 0: + continue + + if coeff.denominator != 1: + max_num_len = max(max_num_len, len(str(abs(coeff.numerator)))) + max_den_len = max(max_den_len, len(str(abs(coeff.denominator)))) + + return {'num': max_num_len, 'den': max_den_len} + +def generate_latex_alignat(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的alignat环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX alignat字符串 + """ + max_widths = calculate_max_widths(k) + + lines = [] + + for r in range(k-1, -2, -1): + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + # 右侧开始 + right_parts = [] + + # 收集所有项数据 + terms_data = [] + for j in range(k): + coeff = calculate_crj(r, j, k) + if coeff == 0: + continue + + is_negative = (coeff < 0) + abs_coeff = abs(coeff) + coeff_str = abs_coeff + + # 下标部分 + offset = j - r + if offset == 0: + subscript = r"i\hphantom{+0}" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" + + v_term = r"\bar{v}_{" + subscript + "}" + terms_data.append((is_negative, coeff_str, v_term)) + + # 组装右侧表达式 + if not terms_data: + right_parts.append("0") + else: + for idx, (is_negative, coeff_str, v_term) in enumerate(terms_data): + # 符号处理 + if idx == 0: + # 第一项:符号与项在同一单元格 + sign = "-" if is_negative else r"\phantom{+}" + term = f"{sign}{coeff_str}{v_term}" if coeff_str else f"{sign}{v_term}" + right_parts.append(term) + else: + # 后续项:符号和项分开对齐 + sign = "-" if is_negative else "+" + term = f"{coeff_str}{v_term}" if coeff_str else v_term + right_parts.append(f"&&{sign} {term}") + + # 组合成行 + line = f" {left} &= " + " ".join(right_parts) + r" \\" + lines.append(line) + + # 计算alignat列数:每项需要2列(符号列和项列),但第一项的符号和项在同一列 + max_terms = max(len([j for j in range(k) if calculate_crj(r, j, k) != 0]) for r in range(k-1, -2, -1)) + alignat_cols = 2 * max_terms - 1 + + content = "\n".join(lines) + return f"\\begin{{alignat}}{{{alignat_cols}}}\n{content}\n\\end{{alignat}}" + +# 主函数示例 +if __name__ == "__main__": + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_alignat(3)) + + print("\n" + "="*60 + "\n") + + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_alignat(4)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01g/testprj.py b/example/weno-coef/crj/python/expand_formula/01g/testprj.py new file mode 100644 index 00000000..37cc5706 --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01g/testprj.py @@ -0,0 +1,167 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m从j+1到k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def generate_latex_alignat(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的alignat环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX alignat字符串 + """ + rows = [] + + # r从k-1到-1(降序) + for r in range(k-1, -2, -1): + terms_info = [] + for j in range(k): + coeff_fraction = calculate_crj(r, j, k) + + if coeff_fraction == 0: + continue + + # 符号和绝对值 + sign = -1 if coeff_fraction < 0 else 1 + abs_coeff = abs(coeff_fraction) + + # 生成系数字符串 + if abs_coeff.denominator == 1: + if abs_coeff.numerator == 1: + coeff_str = "" # 系数为1,省略数字 + else: + coeff_str = str(abs_coeff.numerator) + else: + coeff_str = f"\\frac{{{abs_coeff.numerator}}}{{{abs_coeff.denominator}}}" + + # 生成变量下标 + offset = j - r + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" + + # 变量部分 + v_term = f"\\bar{{v}}_{{{subscript}}}" + + terms_info.append({ + 'sign': sign, + 'coeff_str': coeff_str, + 'v_term': v_term + }) + + # 构建行内容 + if not terms_info: + row = f" v_{{i+1/2}}^{{({r})}} &= 0" + else: + # 左侧 + left_part = f" v_{{i+1/2}}^{{({r})}} &=" + + # 处理每一项 + term_parts = [] + for idx, term_info in enumerate(terms_info): + # 符号处理 + if idx == 0: + # 第一项 + if term_info['sign'] == 1: + sign_str = "\\phantom{+}" + else: + sign_str = "-" + else: + # 后续项 + if term_info['sign'] == 1: + sign_str = "+" + else: + sign_str = "-" + + # 系数和变量 + coeff_str = term_info['coeff_str'] + v_term = term_info['v_term'] + + if coeff_str == "": + term_str = f"{sign_str} {v_term}" + else: + term_str = f"{sign_str} {coeff_str}{v_term}" + + term_parts.append(term_str) + + # 用 && 连接各项(第一项前不加&&) + right_side = term_parts[0] + for part in term_parts[1:]: + right_side += f" && {part}" + + row = f"{left_part} {right_side}" + + rows.append(row) + + # 构建alignat环境 + n_columns = k # 项数 + array_content = " \\\\\n".join(rows) + return f"\\begin{{alignat}}{{{n_columns}}}\n{array_content}\n\\end{{alignat}}" + +# 主函数示例 +if __name__ == "__main__": + # 示例:k=3 时的多行公式输出 + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_alignat(3)) + + print("\n" + "="*60 + "\n") + + # 示例:k=4 时的多行公式输出 + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_alignat(4)) + + print("\n" + "="*60 + "\n") + + # 示例:k=5 时的多行公式输出 + print("="*60) + print("k=5 时的展开公式组(r从4到-1):") + print("="*60) + print(generate_latex_alignat(5)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/KKKKKK01f/testprj.py b/example/weno-coef/crj/python/expand_formula/KKKKKK01f/testprj.py new file mode 100644 index 00000000..d9001b50 --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/KKKKKK01f/testprj.py @@ -0,0 +1,210 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + for m in range(j + 1, k + 1): + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + result += Fraction(numerator, denominator) + + return result + +def generate_aligned_fraction(abs_fraction: Fraction, max_num_len: int, max_den_len: int) -> str: + """ + 生成分数字符串,带有对齐填充 + + 参数: + abs_fraction: 分数的绝对值 + max_num_len: 最大分子字符串长度 + max_den_len: 最大分母字符串长度 + + 返回: + str: 对齐的LaTeX分数字符串 + """ + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + return "" # ±1时省略 + else: + return str(abs_fraction.numerator) + + # 分数情况 + num_str = str(abs_fraction.numerator) + den_str = str(abs_fraction.denominator) + + # 计算需要的填充长度 + num_pad_len = max_num_len - len(num_str) + den_pad_len = max_den_len - len(den_str) + + # 构建填充字符串(使用\hphantom) + # \hphantom{0} 会创建一个与"0"等宽的空白 + num_padding = r"\hphantom{" + "0" * num_pad_len + "}" if num_pad_len > 0 else "" + den_padding = r"\hphantom{" + "0" * den_pad_len + "}" if den_pad_len > 0 else "" + + # 返回带填充的分数 + return f"\\frac{{{num_padding}{num_str}}}{{{den_padding}{den_str}}}" + +def expand_formula_with_values(k: int, r: int, max_widths: dict) -> str: + r""" + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的r参数 + max_widths: 包含全局最大宽度的字典 {'num': int, 'den': int} + + 返回: + str: 展开后的LaTeX字符串 + """ + terms = [] + + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 生成分数字符串(带对齐) + coeff_str = generate_aligned_fraction(abs_fraction, max_widths['num'], max_widths['den']) + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串(使用\hphantom实现宽度对齐) + if offset == 0: + subscript = r"i\hphantom{+0}" # 占位对齐 + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset为负时自动带- + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串 + sign_prefix = "-" if is_negative else "" + + if coeff_str == "": + term = sign_prefix + v_term + else: + term = sign_prefix + coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + if i == 0: + # 第一项直接添加 + formatted_terms.append(term) + else: + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + return f"{left} = {right_side}" + +def generate_latex_array(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的array环境格式化输出,带全局分数对齐 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX array字符串 + """ + # 第一步:计算所有公式的系数,找出全局最大分子分母宽度 + max_num_len = 0 + max_den_len = 0 + + for r in range(k-1, -2, -1): + for j in range(k): + coeff = calculate_crj(r, j, k) + if coeff == 0: + continue + + if coeff.denominator != 1: + max_num_len = max(max_num_len, len(str(abs(coeff.numerator)))) + max_den_len = max(max_den_len, len(str(abs(coeff.denominator)))) + + max_widths = {'num': max_num_len, 'den': max_den_len} + + # 第二步:生成所有对齐的公式 + equations = [] + for r in range(k-1, -2, -1): + eq = expand_formula_with_values(k, r, max_widths) + equations.append(eq) + + # 构建array环境,左对齐 + array_content = " \\\\\n".join([f" {eq}" for eq in equations]) + return f"\\begin{{array}}{{l}}\n{array_content}\n\\end{{array}}" + +# 主函数示例 +if __name__ == "__main__": + # 示例:k=3 时的多行公式输出 + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_array(3)) + + print("\n" + "="*60 + "\n") + + # 示例:k=4 时的多行公式输出 + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_array(4)) + + print("\n" + "="*60 + "\n") + + # 示例:k=5 时的多行公式输出 + print("="*60) + print("k=5 时的展开公式组(r从4到-1):") + print("="*60) + print(generate_latex_array(5)) \ No newline at end of file