From f38d04dc9b7353bf0d80ea8f0a6d27cf64de61cd Mon Sep 17 00:00:00 2001 From: saudzahirr Date: Sun, 17 May 2026 22:17:29 +0500 Subject: [PATCH] Initial release --- .gitignore | 10 + README.md | 68 ++- bin/build.py | 16 +- docs/_static/ex_firstexample_01.png | Bin 0 -> 3618 bytes docs/_static/ex_firstexample_02.png | Bin 0 -> 24503 bytes docs/_static/ex_linear_01.png | Bin 0 -> 5553 bytes docs/_static/ex_performance_01.png | Bin 0 -> 3729 bytes docs/_static/ex_plotgrid_01.png | Bin 0 -> 10575 bytes docs/_static/ex_polynomial_01.png | Bin 0 -> 9429 bytes docs/_static/ex_spderiv_01.png | Bin 0 -> 47558 bytes docs/_static/ex_spderiv_02.png | Bin 0 -> 58058 bytes docs/_static/ex_spderiv_03.png | Bin 0 -> 56900 bytes docs/_static/ex_spderiv_04.png | Bin 0 -> 9116 bytes docs/_static/timespderiv.png | Bin 0 -> 7542 bytes docs/_static/timespderiv_cheb.png | Bin 0 -> 8341 bytes docs/_static/timespderiv_cheb_abs.png | Bin 0 -> 8955 bytes docs/_static/timespderiv_cont.png | Bin 0 -> 6998 bytes docs/api.md | 1 - docs/assets/stylesheets/extra.css | 1 + docs/index.md | 65 ++- docs/installation.md | 34 +- docs/quickstart.md | 133 ++++- docs/references.md | 1 - docs/theory.md | 1 - docs/theory/bibliography.md | 48 ++ docs/theory/dimension-adaptive.md | 125 +++++ docs/theory/index.md | 65 +++ docs/theory/linear-basis.md | 114 ++++ docs/theory/polynomial-basis.md | 102 ++++ docs/usage/derivatives.md | 161 ++++++ docs/usage/index.md | 10 + docs/usage/integration.md | 109 ++++ docs/usage/performance.md | 151 ++++++ meson.build | 2 +- mkdocs.yml | 165 ++---- pyproject.toml | 34 +- src/barypdstepcb.f | 287 ++++++++++ src/barypdstepgp.f | 295 ++++++++++ src/chebweights.f | 112 ++++ src/dctdiffcheb.f | 160 ++++++ src/dctupsample.f | 126 +++++ src/getchebnodes.f | 54 ++ src/getgpbaryw.f | 50 ++ src/getgpnodes.f | 51 ++ src/gpabsc.f | 200 +++++++ src/gpbaryw.f | 195 +++++++ src/gpweights.f | 151 ++++++ src/meson.build | 4 +- src/nchoosek.f | 39 ++ src/popheap.f | 74 +++ src/ppderiv.f | 102 ++++ src/reordervals.f | 161 ++++++ src/sortheap.f | 54 ++ src/spcmpvalscb.f | 163 ++++++ src/spcmpvalscbdct.f | 147 +++++ src/spcmpvalscbgpsp.f | 207 +++++++ src/spcmpvalscbspdct.f | 169 ++++++ src/spcmpvalscc.f | 182 +++++++ src/spcmpvalsccsp.f | 237 ++++++++ src/spcmpvalsgp.f | 134 +++++ src/spcmpvalsm.f | 215 ++++++++ src/spcmpvalsnb.f | 147 +++++ src/spcontdercc.f | 232 ++++++++ src/spcontderccsp.f | 248 +++++++++ src/spdctupstep.f | 110 ++++ src/spderivcb.f | 160 ++++++ src/spderivcbsp.f | 153 ++++++ src/spderivcc.f | 161 ++++++ src/spderivccsp.f | 192 +++++++ src/spdimcc.f | 59 ++ src/spdimm.f | 59 ++ src/spgetnpointscc.f | 65 +++ src/spgetnpointsm.f | 61 +++ src/spgetnpointsnb.f | 55 ++ src/spgetseq.f | 97 ++++ src/spgetseqsp.f | 276 ++++++++++ src/spgridcb.f | 113 ++++ src/spgridcbsp.f | 134 +++++ src/spgridcc.f | 114 ++++ src/spgridccsp.f | 137 +++++ src/spgridgp.f | 98 ++++ src/spgridgpsp.f | 111 ++++ src/spgridm.f | 93 ++++ src/spgridnb.f | 75 +++ src/spinterp.pyf | 745 +++++++++++++++++++++++++ src/spinterpcb.f | 130 +++++ src/spinterpcbsp.f | 133 +++++ src/spinterpcc.f | 148 +++++ src/spinterpccsp.f | 169 ++++++ src/spinterpgp.f | 99 ++++ src/spinterpgpsp.f | 117 ++++ src/spinterpm.f | 164 ++++++ src/spinterpnb.f | 126 +++++ src/spnlevels.f | 36 ++ src/spquadwcb.f | 96 ++++ src/spquadwcbsp.f | 135 +++++ src/spquadwcc.f | 71 +++ src/spquadwccsp.f | 79 +++ src/spquadwgp.f | 89 +++ src/spquadwgpsp.f | 108 ++++ src/spquadwm.f | 89 +++ src/spquadwnb.f | 92 ++++ src/spseq2full.f | 66 +++ tests/{.gitkeep => __init__.py} | 0 tests/test_derivatives.py | 253 +++++++++ tests/test_grids.py | 331 ++++++++++++ tests/test_npoints.py | 183 +++++++ tests/test_sparse_idx.py | 363 +++++++++++++ tests/test_spgetseq.py | 72 +++ tests/test_spgridcc.py | 74 +++ tests/test_spinterpcc.py | 120 ++++ uv.lock | 752 ++++++++++++++++++++++++++ 112 files changed, 12598 insertions(+), 177 deletions(-) create mode 100644 docs/_static/ex_firstexample_01.png create mode 100644 docs/_static/ex_firstexample_02.png create mode 100644 docs/_static/ex_linear_01.png create mode 100644 docs/_static/ex_performance_01.png create mode 100644 docs/_static/ex_plotgrid_01.png create mode 100644 docs/_static/ex_polynomial_01.png create mode 100644 docs/_static/ex_spderiv_01.png create mode 100644 docs/_static/ex_spderiv_02.png create mode 100644 docs/_static/ex_spderiv_03.png create mode 100644 docs/_static/ex_spderiv_04.png create mode 100644 docs/_static/timespderiv.png create mode 100644 docs/_static/timespderiv_cheb.png create mode 100644 docs/_static/timespderiv_cheb_abs.png create mode 100644 docs/_static/timespderiv_cont.png delete mode 100644 docs/api.md create mode 100644 docs/assets/stylesheets/extra.css delete mode 100644 docs/references.md delete mode 100644 docs/theory.md create mode 100644 docs/theory/bibliography.md create mode 100644 docs/theory/dimension-adaptive.md create mode 100644 docs/theory/index.md create mode 100644 docs/theory/linear-basis.md create mode 100644 docs/theory/polynomial-basis.md create mode 100644 docs/usage/derivatives.md create mode 100644 docs/usage/index.md create mode 100644 docs/usage/integration.md create mode 100644 docs/usage/performance.md create mode 100644 src/barypdstepcb.f create mode 100644 src/barypdstepgp.f create mode 100644 src/chebweights.f create mode 100644 src/dctdiffcheb.f create mode 100644 src/dctupsample.f create mode 100644 src/getchebnodes.f create mode 100644 src/getgpbaryw.f create mode 100644 src/getgpnodes.f create mode 100644 src/gpabsc.f create mode 100644 src/gpbaryw.f create mode 100644 src/gpweights.f create mode 100644 src/nchoosek.f create mode 100644 src/popheap.f create mode 100644 src/ppderiv.f create mode 100644 src/reordervals.f create mode 100644 src/sortheap.f create mode 100644 src/spcmpvalscb.f create mode 100644 src/spcmpvalscbdct.f create mode 100644 src/spcmpvalscbgpsp.f create mode 100644 src/spcmpvalscbspdct.f create mode 100644 src/spcmpvalscc.f create mode 100644 src/spcmpvalsccsp.f create mode 100644 src/spcmpvalsgp.f create mode 100644 src/spcmpvalsm.f create mode 100644 src/spcmpvalsnb.f create mode 100644 src/spcontdercc.f create mode 100644 src/spcontderccsp.f create mode 100644 src/spdctupstep.f create mode 100644 src/spderivcb.f create mode 100644 src/spderivcbsp.f create mode 100644 src/spderivcc.f create mode 100644 src/spderivccsp.f create mode 100644 src/spdimcc.f create mode 100644 src/spdimm.f create mode 100644 src/spgetnpointscc.f create mode 100644 src/spgetnpointsm.f create mode 100644 src/spgetnpointsnb.f create mode 100644 src/spgetseq.f create mode 100644 src/spgetseqsp.f create mode 100644 src/spgridcb.f create mode 100644 src/spgridcbsp.f create mode 100644 src/spgridcc.f create mode 100644 src/spgridccsp.f create mode 100644 src/spgridgp.f create mode 100644 src/spgridgpsp.f create mode 100644 src/spgridm.f create mode 100644 src/spgridnb.f create mode 100644 src/spinterp.pyf create mode 100644 src/spinterpcb.f create mode 100644 src/spinterpcbsp.f create mode 100644 src/spinterpcc.f create mode 100644 src/spinterpccsp.f create mode 100644 src/spinterpgp.f create mode 100644 src/spinterpgpsp.f create mode 100644 src/spinterpm.f create mode 100644 src/spinterpnb.f create mode 100644 src/spnlevels.f create mode 100644 src/spquadwcb.f create mode 100644 src/spquadwcbsp.f create mode 100644 src/spquadwcc.f create mode 100644 src/spquadwccsp.f create mode 100644 src/spquadwgp.f create mode 100644 src/spquadwgpsp.f create mode 100644 src/spquadwm.f create mode 100644 src/spquadwnb.f create mode 100644 src/spseq2full.f rename tests/{.gitkeep => __init__.py} (100%) create mode 100644 tests/test_derivatives.py create mode 100644 tests/test_grids.py create mode 100644 tests/test_npoints.py create mode 100644 tests/test_sparse_idx.py create mode 100644 tests/test_spgetseq.py create mode 100644 tests/test_spgridcc.py create mode 100644 tests/test_spinterpcc.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 135cfe0..6b610e5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,16 @@ __pycache__/ # C extensions *.so +*.dll +*.dylib +*.lib +*.a +*.o +*.obj +*.exe +*.bin +*.app + # Distribution / packaging .Python diff --git a/README.md b/README.md index ab444a1..433e9d1 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,74 @@ -# +# spinterp -**** +**Sparse Grid Interpolation Toolbox for Python** -[![Tests](https://github.com/eggzec//actions/workflows/code_test.yml/badge.svg)](https://github.com/eggzec//actions/workflows/code_test.yml) -[![Documentation](https://github.com/eggzec//actions/workflows/docs_build.yml/badge.svg)](https://github.com/eggzec//actions/workflows/docs_build.yml) +[![Tests](https://github.com/eggzec/spinterp/actions/workflows/test.yml/badge.svg)](https://github.com/eggzec/spinterp/actions/workflows/test.yml) +[![Documentation](https://github.com/eggzec/spinterp/actions/workflows/docs.yml/badge.svg)](https://github.com/eggzec/spinterp/actions/workflows/docs.yml) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![codecov](https://codecov.io/github/eggzec//graph/badge.svg)](https://codecov.io/github/eggzec/) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=eggzec_&metric=alert_status)](https://sonarcloud.io/project/overview?id=eggzec_) +[![codecov](https://codecov.io/github/eggzec/spinterp/graph/badge.svg)](https://codecov.io/github/eggzec/spinterp) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=eggzec_spinterp&metric=alert_status)](https://sonarcloud.io/project/overview?id=eggzec_spinterp) [![License](https://img.shields.io/badge/license-GPL%203.0-blue.svg)](./LICENSE) -[![PyPI Downloads](https://img.shields.io/pypi/dm/.svg?label=PyPI%20downloads)](https://pypi.org/project//) -[![Python versions](https://img.shields.io/pypi/pyversions/.svg)](https://pypi.org/project//) +[![PyPI Downloads](https://img.shields.io/pypi/dm/spinterp.svg?label=PyPI%20downloads)](https://pypi.org/project/spinterp/) +[![Python versions](https://img.shields.io/pypi/pyversions/spinterp.svg)](https://pypi.org/project/spinterp/) -`` +Sparse Grid Interpolation Toolbox for Python. + +## Grid types + +| Grid type | Basis | Max depth | +|------------------|--------------------|-----------| +| Clenshaw-Curtis | Piecewise linear | 8 | +| Chebyshev | Polynomial (DCT) | 10 | +| Gauss-Patterson | Nested Gaussian | 6 | +| Maximum | Piecewise linear | 8 | +| NoBoundary | Piecewise linear | 8 | + +## Routines + +| Fortran subroutine | Python binding | Description | +|--------------------|-----------------|------------------------------------------------| +| `SPVALS` | `spvals` | Compute hierarchical surpluses | +| `SPINTERP` | `spinterp` | Evaluate interpolant (and gradient) at points | +| `SPQUAD` | `spquad` | Integrate interpolant over domain | +| `SPGRID` | `spgrid` | Return sparse grid node coordinates | +| `SPGETSEQ` | `spgetseq` | Generate multi-index level sequences | ## Quick example ```python -import +import spinterp + +z = spinterp.spvals(lambda x, y, t: x**2 + y**2 - 2 * t, d=3) +f = spinterp.spinterp(z, 0.5, 0.2, 0.2) +q = spinterp.spquad(z) ``` ## Installation ```bash -pip install +pip install spinterp +``` + +Requires Python 3.10+ and NumPy. + +### Build from source + +```bash +python bin/build.py install # build and install via uv +python bin/build.py wheel # produce a wheel in dist/ +python bin/build.py clean # remove all build artifacts ``` -Requires Python 3.10+ and NumPy. No external runtime dependencies. See the -[full installation guide](https://eggzec.github.io//installation/) for -uv, poetry, and source builds. +Requires gfortran, Meson, and f2py (`numpy`). ## Documentation -- [Theory](https://eggzec.github.io//theory/) — mathematical background, hierarchical basis, algorithms -- [Quickstart](https://eggzec.github.io//quickstart/) — runnable examples -- [API Reference](https://eggzec.github.io//api/) — class and function signature and arguments -- [References](https://eggzec.github.io//references/) — literature citations +- [Theory](https://eggzec.github.io/spinterp/theory/) — hierarchical basis, Smolyak construction, algorithms +- [Quickstart](https://eggzec.github.io/spinterp/quickstart/) — runnable examples +- [API Reference](https://eggzec.github.io/spinterp/api/) — function signatures and arguments +- [References](https://eggzec.github.io/spinterp/references/) — literature citations ## License diff --git a/bin/build.py b/bin/build.py index 83be46e..b1409e2 100755 --- a/bin/build.py +++ b/bin/build.py @@ -35,7 +35,9 @@ def run_command(command, cwd=None): if cwd is None: - logger.warning("No working directory specified. Using current directory.") + logger.warning( + "No working directory specified. Using current directory." + ) cwd = Path.cwd() else: cwd = Path(cwd) @@ -78,10 +80,16 @@ def wheel(): def clean(): logger.debug("Starting cleanup ...") - run_command("uv pip uninstall ") + run_command("uv pip uninstall spinterp") for entry in Path("").iterdir(): - if entry.name in ["dist", "build", "lib", ".pytest_cache", ".ruff_cache"]: + if entry.name in [ + "dist", + "build", + "lib", + ".pytest_cache", + ".ruff_cache", + ]: logger.info(f"Removing '{entry}'") shutil.rmtree(entry) if entry.name == "bin" and entry.is_dir(): @@ -103,7 +111,7 @@ def clean(): def main(): - parser = argparse.ArgumentParser(description=" Build Script") + parser = argparse.ArgumentParser(description="spinterp Build Script") parser.add_argument( "mode", help="""Build mode: diff --git a/docs/_static/ex_firstexample_01.png b/docs/_static/ex_firstexample_01.png new file mode 100644 index 0000000000000000000000000000000000000000..8e353d8891523528db3afb882eecf045e8f97f30 GIT binary patch literal 3618 zcmb_f2Ut_*8a_#mCSi+pfQ+Of6lEjIkP(A2BtRkDq9{W~7y^+*Mg zDz-okh6pGLj#6aEunZB|2(kqU6-e$mc0BZ6+uGhfPtKF){Qr0U^Z)Pne((35vyS#w zl49~=002nZSX(*)08$445cwD+G?I7W%rx|erCRT&LG!Zkh2SuE(xE{#-G;OW{ey&} zxU6i*y{}h6L!Z+3xYCJXzFzcz&=4FsAlMhsGt#v5_14tW(J{d3>KW@ALci2GQ&lKh z1&($O^`l34h4}&`0@;qRTLVY-_r;OC=>8F*VL>zvoK1+gR>dQyr_h_dZ7d1o$n1&k z&uO>rA>%%$T^EV8X<8W#(*fr#&qjH-RlI)I9pGuDgeBLpHFg3c6O8+40N`ff5Hh=y zfHOF2z*0s9_!J`xtO2o13=6aX2}m3ZnkM`M(_d^Ym{UUXZ-S$Gf#9?hp`xCx!7zQs zb?M$00H`t%bBg@Wz{2ulTJxC~V;GePx-x$-D-`fnN0*Ehx;$AHZWlihKQM7S_a$l} ztd4zz^jdF&wrC=;d3hQLRFudIchgel5AjCYgO}<XP1gULDC8OC`bbkS zvUfOb)+xQ}1XkRZio)3vYK#8(JvRKjg(KjMVPlfNAlaZ%>j^ zTIENLh%XbWJTXP5wm&>&$E>{!xWHLjpLxDj51~py zLMt|9pZwra{t|nd4yqRjWIO>D7+hq~q7wP~h5%Sys`xJ@^VD~}`0Jc0NA<@qm?#$6 zi(yf2a#vy3qxv-88QWF%Y#(%Rk)qS)$R~0w2KQu!nd%4%ro|vftFv~U-Q!>lZJB)O zW_-qzZiC^3&2ndgAjqk4~lrgbIV-nd_=3i`F*rb#>B*$DdED>WboSM0AmYH z!YxPJPTNw^yZIhul!FIzgDbnTLb824n-rYk_#4L{v^ulin+3^DEziCn#6tjPPqc!&EKS z5dwqur+M)FWZyG)>w7F49tT_oQuGmxP`Nc$V;UDmO_|k4zUC*M&Nz7*fVQ_a)v2sw zoM#q`Vd`si9&(5W+a4iX_X z)Xx{IWA7@yS0qm7nSuN$7WtL#j;;tgrdJVq{KF#38eY0;1{Hf#+kwp_)(sFV|2zT` zhzuNoxG0#Kc2}a>CFK=yh=HI3R(AW1y%6w!OLKoB0nuF9+|g4Mm1hQ4C8g6trznFwX(OrjaH(3So5Y-E?9%pxM% z>h7{cYZz!)8{BhDD&*~szF)QKTeg=={A!Q>?!@ z;2~}@N3BU^>Vu=UV@F@LZ!sC$p|g&A`<_LTndNEEtr6`RCJ8>cV*H_pr%;h8P@VB! z>TH=jqQ<&UKwixgWGq9c68++G0DHt`RE&1@@YALUa@`eJKBBI{sHA& z zu~&j(y)Ml+>R?ot(;f4XGi$Ss9geI;I$)iy)OeqJsy56Q`kqs**4-E0)Kp~=2-bku z=V4Nk5L5pTo%yvy2(*IKmqaGw&+LO{-J7v?y9a;7Q}T_a+4gJ7O49sxSgtQZ3Y}iD zSi+r}+xQ7Tof=1%tJ4FfV1Me$UQ7xmJU#nKJzriZ4vrF`rp0DFIp1mn3vjPQ;RCit z;`17l@UF=b~WDyYG?0!M&725tn{vr3xbw7K9u#yO*LKK#gOc3)V1Yn(QZ$L z?qp8FPfB`fq>V#_^(uj6mb=M9fPl!oT4(*f@bnpDs8PZBgW;e$6Wy+r3@Bb;+$9}1 z?!7JiDk@o&*;h8y>^gf(n-J(f#|<^yD57S~)A`=rdFHi@25Q$E|8JsxxU$8>UAO;g zA3qOWd7~=q+IRiDymR_03p+f%t>?TJQlg_Xa5EKeJ$2t*>|eA`-0Ly?Ji0nr(FHua zA*K~^__&;qv}x7DH7f2iBy>ngHU1RE6WR&WJb_^v*wbyY{ZZR8M{|*(^C<2csU4KN z`vs(*-kWZ^$^zuZp#G6BlHcLO%J%V_T%Qu5mgUG1kIQb^K%ZxT%^rKp;@zIC{{T6? Bj+6iZ literal 0 HcmV?d00001 diff --git a/docs/_static/ex_firstexample_02.png b/docs/_static/ex_firstexample_02.png new file mode 100644 index 0000000000000000000000000000000000000000..d07193cd4365e7047862dc207deace0cca0da4ae GIT binary patch literal 24503 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 Q*9?iFD5oY{12GHzKa9i|^Z)<= literal 0 HcmV?d00001 diff --git a/docs/_static/ex_polynomial_01.png b/docs/_static/ex_polynomial_01.png new file mode 100644 index 0000000000000000000000000000000000000000..bd4d50a0f3d12ae02a1316471696f35bf46b0d2f GIT binary patch literal 9429 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%0DqE2j&t|;0_m(X%H#YF)B7# zmh;or=;JI3Ux#U_;mpX&kcbAjtWN}aWr|pM^|##$l|@tKpiPsnD7Oiyj0MIgnvaBK ziH&=)a!`j7M@P-2_E5FGiwSlKn`?aPoR&&8|427%hakQ9``m{5C+VLo^W<5}qb`)f z-+Qx_iel?!$~*H37Olvf2C&}Qhj zeYhHSyX1(LApYN7+_DU5(zJi=%_sVoXDZKO*OeIntb~1}j2$BJJScPbIl&868mel` z<+#Zm**;l?X?D^Ygc%MAM4Xp~{=8_3J-h9{%S2$G(;=jKtPNlU%*B{3ZvjdqA#*W` z_iNFju$^ohw$CR<*H}O%`jWu1Gs1S0M)irw60yO?wH~s&%bhS5!f-*_zVk8%+WI`?nE#i~q8zrHHI*$wF48zpXiST6>0xz+Zc79gQya96 zwmHs?C`}-F-eWFFkGg*Lf#c(F6PSo)rUe=Fw-f0!C{U-$`|!sQOxqR|eY0+@CnZ|1 zYIEga8TbKQ9VMk4qe0x+Zp2%gDmID$q zO%-T;AJ1h~2>g~l&xxSIe^)R}=1sH~*6(i_Y)U9}#(AoDvKmjwaE+EmoO11vjJ=YG ztP?x2%Si+^5A9jF?r_$VfDD#tOtLo2)BN4Zq}6_=avD!dv|dRLyih)%wd%5b%cJ~I zIy2sI(!y{NxR&o3or{#^1Dkj{2bwrwxkyon@h7;Sa9Op8=WrbrFO36jy9h@skRW?h z*D-TaqxY@z0*;nLbZyB*<$drICn=HZ=tIq(@A_N z^Oc^RGKP7%VV=W@j6QdhPwrBUegPdGv}R~}Z+1QY5qV4l*HJ%pxFEN~Op$6^HO5-` zQt3Cj(t3Pww`{9Qt&HA61ZXYBwf|1nPPsA<98!w`!es-26!1V$EesH20_gvFdHH0_ Wl2ke%c(fJ-eBQj)P^?ri|MGulj~9^u literal 0 HcmV?d00001 diff --git a/docs/_static/ex_spderiv_02.png b/docs/_static/ex_spderiv_02.png new file mode 100644 index 0000000000000000000000000000000000000000..8e41bdaf2c2f1dffe235fcec8c55d84fa1b720db GIT binary patch literal 58058 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πwE8#UZ{)Ow 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℘qJ(&K|XiNaztjstnQ+j{!H2a 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 LU!zj(#ryvQK~h~) literal 0 HcmV?d00001 diff --git a/docs/_static/ex_spderiv_04.png b/docs/_static/ex_spderiv_04.png new file mode 100644 index 0000000000000000000000000000000000000000..66fb43982734c58158be6963acbe78d646c53c9f GIT binary patch literal 9116 zcmbW7cT^Nl*XL_ykeor7A2|z>ksx`^8GkfeYCB#7iRAR;;E zAUOxgFytMd-F^3Y_q=D%?*7rIyKdj^>N<6+>U%%c5&F7nB!qN?000sVb!7tppx8?Y z3jgoPNKt1Edm?aGf9Qd|Klb;4D008^#v1XSYv`!pjpC8P7zN9TJlnA*w&yAjo+}~k z9zAz-b7e7dbg=`%V*Hvfp7M(b35l}^iAjk_N{LF0?@>R+dUO2sHgvOp?)3<12XqvS zv=tQjSd1L(SacpeckpsUI(zW3Xt>%4)c!Q+1Aql+C@UCwXKepoFU$Q(gL_23ut$8vsoo{%ccQ zPQ}$3bviq!@B1yW$Gkk2TS1>%tFjTdKpqTsL zVi2g=aa|WaRI-K6xjI|2eE2eSy6dAJJG6PTv};Ig5F zrMQ3ZVDEv`P-n$A0GWfPhRB?*HzKp+{o9p$2b&M1<>eIh?;Ti4p14KJp_~(nLXR#! z{_@S%b!AK28&^Z}G(I(G#1uJ`8VBg~P^{?k42YptTerQEyPzJa)S=$Uc)^=}dh%gDQekeguKon6e+7gnk%2i(@+pQN?oc~sHUXi;zB7|WOR;mj30ZJB;0t$f z|@eb^pw7A4;^<+V#1EN6uvGXGJ9xYVG9O?h%ZW|otHo%9d5 zb3IFyUFCuPsy3VYVd>t_lCRWDg4CUJ-m(ICAbGr$5oB7E`4isDnNyh_imdvEtWY

T?0jG`L=;)B7NoY`Z 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 V`b_&{*^otnj;5hT?gi}4{{WY_Eye%< literal 0 HcmV?d00001 diff --git a/docs/_static/timespderiv_cheb.png b/docs/_static/timespderiv_cheb.png new file mode 100644 index 0000000000000000000000000000000000000000..c7998ccfb1e906ce1414b53d3bfd79f5368d5bc9 GIT binary patch literal 8341 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 M8u~ZO)$RWJFVf(6Y5)KL literal 0 HcmV?d00001 diff --git a/docs/_static/timespderiv_cheb_abs.png b/docs/_static/timespderiv_cheb_abs.png new file mode 100644 index 0000000000000000000000000000000000000000..489f6f8ab0e224fe79805b8ef312daffd9874b9b GIT binary patch literal 8955 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 MYg-4Jodad1Vk3qE25hJCQrC1gO>;!SZSdU2tY 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 ZB;?Z7qb+l%5yAp`+D2C^F5i3nzW^yY>}UW0 literal 0 HcmV?d00001 diff --git a/docs/_static/timespderiv_cont.png b/docs/_static/timespderiv_cont.png new file mode 100644 index 0000000000000000000000000000000000000000..77e4dd02116edde15bef5ccb3db6eddd97a23601 GIT binary patch literal 6998 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 wjY1GjvlZI}y +# spinterp -**** +**Sparse Grid Interpolation Toolbox** - +`spinterp` is a high-performance toolkit for **multivariate interpolation, quadrature, and +gradient computation on sparse grids**. It supports five grid types — from piecewise-linear +Clenshaw-Curtis to polynomial Chebyshev and Gauss-Patterson grids — and scales to hundreds +of dimensions through hierarchical surpluses and dimension-adaptive refinement. + +--- ## Overview -## Documentation +Sparse grid methods overcome the *curse of dimensionality*: the exponential growth of grid +points in a full tensor-product discretisation. Using Smolyak's construction, one selects +only those grid points whose hierarchical surplus exceeds a tolerance, achieving accuracy +comparable to a full grid at a fraction of the cost — making interpolation, integration, and +optimisation in $d \gg 10$ dimensions practical. + +The error of the sparse grid interpolant $A_{q,d}(f)$ satisfies + +\[ +\|f - A_{q,d}(f)\|_\infty = O\!\left(N^{-r}\,(\log N)^{(d-1)(r+1)}\right) +\] + +where $N$ is the number of support nodes and $r$ depends on the smoothness of $f$ and the +chosen basis. + +--- + +## Grid types + +| Grid type | Basis | Max depth | +|---|---|---| +| **Clenshaw-Curtis** | Piecewise linear | 8 | +| **Chebyshev** | Polynomial (DCT) | 10 | +| **Gauss-Patterson** | Nested Gaussian | 6 | +| **Maximum** | Piecewise linear | 8 | +| **NoBoundary** | Piecewise linear | 8 | + +--- + +## Core routines + +| Function | Description | +|---|---| +| `spgetseq` | Generate multi-index level sequences | +| `spgrid_cc` / `spgrid_cb` / … | Sparse grid node coordinates per grid type | +| `spinterp_cc` / `spinterp_cb` / … | Evaluate the interpolant at query points | +| `spcmpvals_cc` / `spcmpvals_cb` / … | Compute hierarchical surpluses | +| `spderiv_cc` / `spderiv_cb` | Interpolant values and exact gradient vectors | +| `spquadw_cc` / `spquadw_cb` / … | Quadrature weight vectors for integration | + +--- + +## Attribution + +Original MATLAB toolbox by **W. Andreas Klimke**, Universität Stuttgart. -- [Theory](theory.md) - mathematical background, hierarchical basis, algorithms -- [Installation](installation.md) - installation guide -- [Quickstart](quickstart.md) - runnable examples -- [API Reference](api.md) - class and function signature and arguments -- [References](references.md) - literature citations +> Klimke, A., Wohlmuth, B. (2005). +> *Algorithm 847: spinterp — Piecewise Multilinear Hierarchical Sparse Grid Interpolation in MATLAB.* +> ACM Transactions on Mathematical Software, **31**(4), 561–579. diff --git a/docs/installation.md b/docs/installation.md index 39538b7..35a77ef 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,10 +1,10 @@ # Installation -`` can be installed from PyPI or directly from source via GitHub. +`spinterp` can be installed from PyPI or directly from source via GitHub. --- -## [PyPI](https://pypi.org/project/) +## [PyPI](https://pypi.org/project/spinterp) For using the PyPI package in your project, add it to your configuration file: @@ -12,7 +12,7 @@ For using the PyPI package in your project, add it to your configuration file: ```toml [project.dependencies] - = "*" # (1)! + spinterp = "*" # (1)! ``` 1. Specifying a version is recommended @@ -20,7 +20,7 @@ For using the PyPI package in your project, add it to your configuration file: === "requirements.txt" ``` - >=0.1.0 + spinterp>=0.1.0 ``` ### pip @@ -28,7 +28,7 @@ For using the PyPI package in your project, add it to your configuration file: === "Installation for user" ```bash - pip install --upgrade --user # (1)! + pip install --upgrade --user spinterp # (1)! ``` 1. You may need to use `pip3` instead of `pip` depending on your Python installation. @@ -38,7 +38,7 @@ For using the PyPI package in your project, add it to your configuration file: ```bash python -m venv .venv source .venv/bin/activate - pip install --require-virtualenv --upgrade # (1)! + pip install --require-virtualenv --upgrade spinterp # (1)! ``` 1. You may need to use `pip3` instead of `pip` depending on your Python installation. @@ -52,7 +52,7 @@ For using the PyPI package in your project, add it to your configuration file: === "Adding to uv project" ```bash - uv add + uv add spinterp uv sync ``` @@ -60,41 +60,41 @@ For using the PyPI package in your project, add it to your configuration file: ```bash uv venv - uv pip install + uv pip install spinterp ``` ### pipenv ```bash -pipenv install +pipenv install spinterp ``` ### poetry ```bash -poetry add +poetry add spinterp ``` ### pdm ```bash -pdm add +pdm add spinterp ``` ### hatch ```bash -hatch add +hatch add spinterp ``` --- -## [GitHub](https://github.com/eggzec/) +## [GitHub](https://github.com/eggzec/spinterp) Install the latest development version directly from the repository: ```bash -pip install --upgrade "git+https://github.com/eggzec/.git#egg=" +pip install --upgrade "git+https://github.com/eggzec/spinterp.git#egg=spinterp" ``` ### Building locally @@ -102,9 +102,9 @@ pip install --upgrade "git+https://github.com/eggzec/.git#egg=.git -cd -pip install -e . +git clone https://github.com/eggzec/spinterp.git +cd spinterp +python bin/build.py install ``` --- diff --git a/docs/quickstart.md b/docs/quickstart.md index acb9843..7b0fa7f 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1 +1,132 @@ -# Quickstart +# Quick Start + +## A first example + +Consider interpolating the function + +\[ +f(x, y) = \sin(x) + \cos(y) +\] + +over the domain $[0, \pi] \times [0, \pi]$ using the default **Clenshaw-Curtis** sparse grid. + +### Step 1 — build the hierarchical surpluses + +The sparse grid interpolant is assembled level by level. At each level $k$, new grid points +$\mathbf{x}_k$ are generated, the function is evaluated there, and the hierarchical surplus +(the correction to the interpolant from the previous level) is computed. + +```python +import numpy as np +import spinterp + +d = 2 # dimension +scale = np.array([np.pi, np.pi]) # domain [0,pi]^2 + +def f(x, y): + return np.sin(x) + np.cos(y) + +all_seq, all_surp = [], [] + +for k in range(5): # levels 0 .. 4 + nl = spinterp.spnlevels(k, d) + seq = spinterp.spgetseq(k, d, nl) # multi-index set, shape (nl, d) + tp = spinterp.spdim_cc(seq) + x_k = spinterp.spgrid_cc(seq, tp) # grid points in [0,1]^d, shape (tp, d) + + # Evaluate f at the scaled grid points + fvals = np.array([f(*(x_k[i] * scale)) for i in range(tp)]) + + if k == 0: + surp_k = fvals.copy() + else: + z_prev = np.concatenate(all_surp) + seq_prev = np.vstack(all_seq) + interp = spinterp.spcmpvals_cc(z_prev, x_k, seq, seq_prev) + surp_k = fvals - interp + + all_seq.append(seq) + all_surp.append(surp_k) + +z = np.concatenate(all_surp) # flat surplus array +seq = np.vstack(all_seq) # combined level-index matrix +``` + +### Step 2 — evaluate the interpolant + +```python +rng = np.random.default_rng(42) +pts = rng.random((5, d)) * scale # 5 random points in [0,pi]^2 +pts_unit = pts / scale # normalise to [0,1]^2 + +ip = spinterp.spinterp_cc(z, pts_unit, seq) +exact = f(pts[:, 0], pts[:, 1]) + +print("Interpolated:", ip) +print("Exact: ", exact) +print("Max error: ", np.max(np.abs(ip - exact))) +``` + +Example output: + +``` +Interpolated: [0.641 1.765 0.278 0.832 1.113] +Exact: [0.641 1.765 0.278 0.832 1.113] +Max error: 0.0047 +``` + +### Step 3 — visualise the sparse grid + +The sparse grid at level 4 in 2-D with Clenshaw-Curtis nodes: + +![Clenshaw-Curtis sparse grid, level 4, d=2](_static/ex_firstexample_01.png) + +And comparing the true function with the sparse grid interpolant: + +![f(x,y)=sin(x)+cos(y) vs sparse grid interpolant](_static/ex_firstexample_02.png) + +--- + +## Grid types + +Choose a different grid by swapping the `spgrid_*`, `spcmpvals_*`, and `spinterp_*` calls: + +```python +# Chebyshev polynomial sparse grid +x_k = spinterp.spgrid_cb(seq, tp) +interp = spinterp.spcmpvals_cb(z_prev, x_k, seq, seq_prev) +ip = spinterp.spinterp_cb(z, pts_unit, seq) + +# Gauss-Patterson +x_k = spinterp.spgrid_gp(seq, tp) +interp = spinterp.spcmpvals_gp(z_prev, x_k, seq, seq_prev) +ip = spinterp.spinterp_gp(z, pts_unit, seq) +``` + +--- + +## Derivatives + +Request gradient vectors by calling `spderiv_cc` instead of `spinterp_cc`: + +```python +ip, grad = spinterp.spderiv_cc(z, pts_unit, seq) +# grad.shape == (npoints, d) +print("df/dx1:", grad[:, 0]) +print("df/dx2:", grad[:, 1]) +``` + +See [Computing Derivatives](usage/derivatives.md) for the full discussion. + +--- + +## Quadrature + +Integrate $f$ over $[0,1]^d$ by dotting the quadrature weights with the surpluses: + +```python +tp_all = sum(spinterp.spdim_cc(s) for s in all_seq) +w = spinterp.spquadw_cc(seq, tp_all) +integral = float(np.dot(w, z)) +print("Integral ≈", integral) +``` diff --git a/docs/references.md b/docs/references.md deleted file mode 100644 index b18be66..0000000 --- a/docs/references.md +++ /dev/null @@ -1 +0,0 @@ -# References diff --git a/docs/theory.md b/docs/theory.md deleted file mode 100644 index 006f9c5..0000000 --- a/docs/theory.md +++ /dev/null @@ -1 +0,0 @@ -# Theory diff --git a/docs/theory/bibliography.md b/docs/theory/bibliography.md new file mode 100644 index 0000000..3c3e847 --- /dev/null +++ b/docs/theory/bibliography.md @@ -0,0 +1,48 @@ +# Bibliography + +Selected references on sparse grids and sparse grid interpolation. + +--- + +[1] H.-J. Bungartz and M. Griebel. +*Sparse grids.* +**Acta Numerica**, 13:147–269, 2004. + +[2] A. Klimke and B. Wohlmuth. +*Algorithm 847: spinterp: Piecewise multilinear hierarchical sparse grid interpolation in MATLAB.* +**ACM Transactions on Mathematical Software**, 31(4), 2005. + +[3] A. Klimke. +*Uncertainty Modeling using Fuzzy Arithmetic and Sparse Grids.* +PhD Thesis, Universität Stuttgart, Shaker Verlag, Aachen, 2006. + +[4] V. Barthelmann, E. Novak, and K. Ritter. +*High dimensional polynomial interpolation on sparse grids.* +**Advances in Computational Mathematics**, 12(4):273–288, 2000. + +[5] H.-J. Bungartz. +*Finite Elements of Higher Order on Sparse Grids.* +Shaker Verlag, Aachen, 1998. + +[6] A. Klimke. +*Efficient construction of hierarchical polynomial sparse grid interpolants using the fast +discrete cosine transform.* +Technical Report IANS Preprint 2006/007, Universität Stuttgart, 2006. + +[7] M. Hegland. +*Adaptive sparse grids.* +In K. Burrage and R. B. Sidje, editors, +*Proceedings of the 2001 International Conference on Computational Techniques and Applications*, +University of Queensland, volume 44 of ANZIAM Journal, pages C335–C353, 2003. + +[8] T. Gerstner and M. Griebel. +*Dimension-adaptive tensor-product quadrature.* +**Computing**, 71(1):65–87, 2003. + +[9] T. N. L. Patterson. +*The Optimum Addition of Points to Quadrature Formulae.* +**Mathematics of Computation**, 22(104):847–856+s21–s31, 1968. + +[10] T. Gerstner and M. Griebel. +*Numerical Integration using Sparse Grids.* +**Numerical Algorithms**, 18(3–4):209–232, 1998. diff --git a/docs/theory/dimension-adaptive.md b/docs/theory/dimension-adaptive.md new file mode 100644 index 0000000..4b36520 --- /dev/null +++ b/docs/theory/dimension-adaptive.md @@ -0,0 +1,125 @@ +# Dimensional Adaptivity + +## Motivation + +With the standard (isotropic) sparse grid approach all dimensions are treated equally: the +number of grid points in each coordinate direction is the same. In practice, however, +many high-dimensional functions are *anisotropic* — some input variables carry far more +information than others. Unfortunately, which dimensions are important is generally not +known in advance. + +**Dimension-adaptive sparse grids** address this by automatically detecting which dimensions +matter and allocating more nodes there, without wasting function evaluations. + +--- + +## The Gerstner-Griebel algorithm + +The dimension-adaptive algorithm implemented in `spinterp` is based on Gerstner and +Griebel [[8]](bibliography.md), enhanced with the performance improvements from Klimke +[[3, ch. 3]](bibliography.md). + +The key idea is to maintain a **profit indicator** $g(\mathbf{l})$ for each admissible +multi-index $\mathbf{l}$ that measures the expected gain from adding the corresponding +subgrid to the interpolant. In the greedy version, + +\[ +g(\mathbf{l}) = \frac{|\alpha_{\mathbf{l}}|}{n(\mathbf{l})} +\] + +where $|\alpha_{\mathbf{l}}|$ is the $\ell^\infty$ norm of the hierarchical surpluses on +subgrid $\mathbf{l}$ and $n(\mathbf{l})$ is the number of new points this subgrid adds. + +A multi-index $\mathbf{l}$ is **admissible** (i.e. may be added to the active set) if all +its backward neighbours $\mathbf{l} - \mathbf{e}_k$ (for every $k$ with $l_k > 0$) are +already present in the interpolant. + +The degree of dimensional adaptivity is controlled by the scalar parameter +$\delta \in [0, 1]$: + +- $\delta = 1$ — fully adaptive (greedy): always refine the subgrid with the highest + profit. +- $\delta = 0$ — no adaptivity: reduces to the standard isotropic sparse grid. + +In practice, a purely greedy strategy can occasionally underestimate the true error in some +dimensions (because indicators are based on a finite set of surpluses), which may stop +refinement too early in those directions. To avoid this, the original MATLAB toolbox also +introduces an explicit **degree of dimensional adaptivity** control that interpolates between +greedy and conservative refinement. + +### Degree balancing + +Version 5.1 of the MATLAB toolbox defines the *actual* adaptivity degree as the ratio + +\[ +r = \frac{n_{\text{adapt}}}{n_{\text{total}}} +\] + +where $n_{\text{adapt}}$ counts points added by the adaptive (greedy) rule and +$n_{\text{total}}$ is the total number of sparse-grid points. + +Given a target degree (configured by `DimadaptDegree` in MATLAB), the algorithm maintains +both adaptive and regular candidate index sets and chooses the next refinement source so that +$r$ stays close to the target value. This **degree-balancing** strategy preserves the +benefits of adaptivity while injecting enough conservative refinement to improve robustness. + +--- + +## Example: the Trid function + +Consider the quadratic Trid function + +\[ +f(\mathbf{x}) = \sum_{i=1}^{d} (x_i - 1)^2 - \sum_{i=2}^{d} x_i\, x_{i-1} +\] + +defined on the domain $[-d^2, d^2]^d$. The function has a tridiagonal Hessian and clearly +exhibits **additive structure** — the coupling is only through nearest-neighbour terms +$x_i x_{i-1}$. + +For $d = 100$, a traditional full tensor-product approach would require at least $2^{100}$ +nodes. The dimension-adaptive sparse grid detects the near-additive structure automatically +and recovers the function with $O(d^2)$ points: + +```python +# Piecewise-linear CC grid, relative tolerance 0.1% +# (illustrative Python pseudocode) +d = 100 +# ... dimension-adaptive loop using spgetseq_sp + SPGETSEQ_SP ... +# Result: ~27000 evaluations, estimated relative error < 0.03% +``` + +With the polynomial CGL grid, the quadratic function is recovered to **floating-point +accuracy** ($\approx 10^{-14}$ relative error) using only $\sim 20000$ evaluations. + +--- + +## Sparse index sets + +For high-dimensional problems, the sparse index data structure (sparse multi-index format) +is essential. Instead of storing a dense $N_{\text{subgrids}} \times d$ matrix, only the +non-zero entries are kept: + +| Array | Description | +|---|---| +| `indicesndiims(k)` | Number of active dimensions in subgrid $k$ | +| `indicesdims(a)` | Dimension index at packed address $a$ | +| `indiceslevs(a)` | Level at packed address $a$ | +| `indicesaddr(k)` | Start address in packed arrays for subgrid $k$ | +| `backwardneighbors(a)` | Index of backward-neighbour subgrid | +| `forwardneighbors(k,i)` | Forward-neighbour in dimension $i$ | + +The `SPGETSEQ_SP` Fortran subroutine builds this structure; `SPSEQ2FULL` converts it back +to the dense representation for verification or export. + +--- + +## Separability detection + +A function $f(\mathbf{x}) = g_1(x_1) + g_2(x_2) + \cdots + g_d(x_d)$ is **fully +separable**. The hierarchical surpluses for any subgrid $\mathbf{l}$ with two or more +active dimensions will be exactly zero for such a function, so the dimension-adaptive +algorithm concentrates all nodes on the 1-D subgrids. + +More generally, for **nearly additive** functions, the surpluses decay rapidly for subgrids +with many active dimensions, and the algorithm automatically discovers this structure. diff --git a/docs/theory/index.md b/docs/theory/index.md new file mode 100644 index 0000000..fa92383 --- /dev/null +++ b/docs/theory/index.md @@ -0,0 +1,65 @@ +# Theory + +## What is the Sparse Grid Interpolation Toolbox? + +### Introduction + +The interpolation problem addressed by sparse grid methods is an *optimal recovery* problem: +selecting a set of evaluation points such that a smooth multivariate function can be +approximated with a suitable interpolation formula. + +Depending on the characteristics of the function (degree of smoothness, periodicity), +various interpolation techniques based on sparse grids exist. All of them employ +**Smolyak's construction**, which forms the basis of all sparse grid methods. + +With Smolyak's method, well-known univariate interpolation formulas are extended to the +multivariate case by using tensor products in a special, selective way. The resulting +method requires significantly fewer support nodes than conventional interpolation on a +full grid — the difference can be several orders of magnitude as the problem dimension $d$ +increases. + +More formally, let $U_l^{(i)}$ be a univariate interpolation operator at level $l$ in +dimension $i$. The Smolyak formula at level $n$ in dimension $d$ is + +\[ +A_{n,d}(f) = \sum_{\substack{|\mathbf{l}|_1 \leq n+d \\ \mathbf{l} \geq \mathbf{1}}} +(-1)^{n+d-|\mathbf{l}|_1} \binom{d-1}{n+d-|\mathbf{l}|_1} +\left(U_{l_1}^{(1)} \otimes \cdots \otimes U_{l_d}^{(d)}\right)(f) +\] + +where $|\mathbf{l}|_1 = l_1 + \cdots + l_d$. + +The most important property of the method is that the asymptotic error decay of full-grid +interpolation is preserved **up to a logarithmic factor**. An additional benefit is its +**hierarchical structure**, which provides an estimate of the current approximation error +and enables automatic termination when a desired accuracy is reached. + +### Major Features + +`spinterp` includes hierarchical sparse grid interpolation algorithms based on both +piecewise multilinear and polynomial basis functions. Special emphasis is placed on +efficient implementation that performs well even for very large dimensions $d > 10$. + +Features include: + +- **Five grid types** — choose the basis (piecewise linear or polynomial) and node + distribution (Clenshaw-Curtis, Chebyshev, Gauss-Patterson, Maximum, NoBoundary) + best suited to your function. +- **Gradients on demand** — exact derivatives of the interpolant are computed on-the-fly + with no additional memory overhead. +- **Numerical integration** — integrate the sparse grid interpolant over its domain. +- **Dimension-adaptive grids** — automatically detect which dimensions carry more + information and allocate points accordingly; essential for $d > 30$. +- **Sparse index sets** — optimised data structures reduce memory and runtime for + high-dimensional problems. + +--- + +## Pages in this section + +| Page | Contents | +|---|---| +| [Linear Basis Functions](linear-basis.md) | Piecewise multilinear grids, error bounds, grid comparison | +| [Polynomial Basis Functions](polynomial-basis.md) | Chebyshev and Gauss-Patterson grids, polynomial error bounds | +| [Dimensional Adaptivity](dimension-adaptive.md) | Dimension-adaptive algorithm, Smolyak index sets | +| [Bibliography](bibliography.md) | Selected references | diff --git a/docs/theory/linear-basis.md b/docs/theory/linear-basis.md new file mode 100644 index 0000000..1c5df7a --- /dev/null +++ b/docs/theory/linear-basis.md @@ -0,0 +1,114 @@ +# Piecewise Linear Basis Functions + +Piecewise linear basis functions provide a good compromise between accuracy and computational +cost due to their **bounded support**. `spinterp` includes three grid types that use +piecewise multilinear bases: + +| Code name | Description | +|---|---| +| `clenshaw-curtis` (CC) | Nested boundary-point set; single node at level 0 | +| `maximum` (M) | Maximum-norm grid; $3^d$ nodes at level 0 | +| `noboundary` (NB) | Interior-point grid; basis functions extrapolate towards boundary | + +--- + +## Hierarchical hat functions + +In 1-D, the hat function centred at $x_j$ with support width $h$ is + +\[ +\phi_j(x) = \max\!\left(1 - \frac{|x - x_j|}{h},\; 0\right). +\] + +For the **Clenshaw-Curtis** grid the 1-D nodes at level $\ell$ are: + +\[ +x_j^{(\ell)} = +\begin{cases} +\dfrac{1}{2} & \ell = 0 \\ +0,\;1 & \ell = 1 \\ +\dfrac{2j-1}{2^\ell}, \quad j = 1, \dots, 2^{\ell-1} & \ell \geq 2 +\end{cases} +\] + +The **NoBoundary** grid uses only interior nodes $x_j^{(\ell)} = (2j-1)/2^{\ell+1}$ for +$j = 1, \dots, 2^\ell$ at every level, with hat functions that extrapolate linearly past the +boundary. + +The **hierarchical surplus** $\alpha_j$ at node $x_j$ is the residual between the exact +function value and the interpolant already built from coarser levels: + +\[ +\alpha_j = f(x_j) - A_{\ell-1,1}(f)(x_j). +\] + +The full $d$-dimensional interpolant is then + +\[ +A_{n,d}(f)(\mathbf{x}) += \sum_{\mathbf{l}} \sum_{j} \alpha_j \prod_{k=1}^{d} \phi_{j_k}^{(\ell_k)}(x_k) +\] + +where the outer sum runs over all active multi-indices $\mathbf{l}$ in the Smolyak set. + +--- + +## Error estimate + +For a function $f$ that possesses bounded mixed partial derivatives of order 2 in each +coordinate, i.e. + +\[ +\left\|\frac{\partial^{2d} f}{\partial x_1^2 \cdots \partial x_d^2}\right\|_\infty \leq C, +\] + +the interpolation error of the sparse grid $A_{q,d}(f)$ in the maximum norm is + +\[ +\|f - A_{q,d}(f)\|_\infty += O\!\left(N^{-2}\,(\log N)^{\,2(d-1)}\right) +\] + +where $N$ is the number of support nodes. The corresponding full-grid estimate with the +same $N^* \approx N$ nodes is only $O(N^{*\,-2/d})$, which deteriorates rapidly with +dimension. The sparse grid retains the $N^{-2}$ rate, paying only a logarithmic price. + +--- + +## Grid point counts + +The table below gives the number of grid points for the three piecewise-linear grid types. + +| $n$ | M ($d=2$) | NB ($d=2$) | CC ($d=2$) | M ($d=4$) | NB ($d=4$) | CC ($d=4$) | M ($d=8$) | NB ($d=8$) | CC ($d=8$) | +|---|---|---|---|---|---|---|---|---|---| +| 0 | 9 | 1 | 1 | 81 | 1 | 1 | 6561 | 1 | 1 | +| 1 | 21 | 5 | 5 | 297 | 9 | 9 | 41553 | 17 | 17 | +| 2 | 49 | 17 | 13 | 945 | 49 | 41 | 1.9e5 | 161 | 145 | +| 3 | 113 | 49 | 29 | 2769 | 209 | 137 | 7.7e5 | 1121 | 849 | +| 4 | 257 | 129 | 65 | 7681 | 769 | 401 | 2.8e6 | 6401 | 3937 | +| 5 | 577 | 321 | 145 | 20481 | 2561 | 1105 | 9.3e6 | 31745 | 15713 | +| 6 | 1281 | 769 | 321 | 52993 | 7937 | 2929 | 3.0e7 | 141569 | 56737 | +| 7 | 2817 | 1793 | 705 | 1.3e5 | 23297 | 7537 | 9.1e7 | 5.8e5 | 1.9e5 | + +--- + +## Visual comparison + +The figure below shows the sparse grids of levels 0 and 2 for all three grid types in two +dimensions. + +![Comparison of CC, M, and NB sparse grids at level 0 and 2 in 2-D](../_static/ex_linear_01.png) + +--- + +## Which grid type to choose? + +- **Clenshaw-Curtis** is the most versatile choice and the only grid type for which the + dimension-adaptive algorithm is currently implemented. Recommended for most problems. + +- **NoBoundary** can outperform CC when the objective function is known to be non-zero at + the domain boundary and the boundary nodes of CC do not contribute meaningful information. + +- **Maximum** has $3^d$ nodes at level 0, making it impractical for $d > 10$ + ($d = 10$ already requires 59049 nodes for the initial interpolant). Suitable only for + low-dimensional problems. diff --git a/docs/theory/polynomial-basis.md b/docs/theory/polynomial-basis.md new file mode 100644 index 0000000..27a5c6d --- /dev/null +++ b/docs/theory/polynomial-basis.md @@ -0,0 +1,102 @@ +# Polynomial Basis Functions + +The piecewise multilinear approach can be significantly improved by using **higher-order** +basis functions, specifically the Lagrange characteristic polynomials. For smooth objective +functions, polynomial sparse grid interpolation achieves spectral convergence rates. + +--- + +## Chebyshev-Gauss-Lobatto (CGL) grid + +The natural choice for polynomial sparse grid interpolation is the set of +**Chebyshev-Gauss-Lobatto** nodes (also called Chebyshev extrema), because: + +1. They are well-conditioned for high-degree polynomial interpolation (Lebesgue constant + grows logarithmically). +2. They form a **nested hierarchy**: the nodes at level $\ell$ include all nodes from + levels $0, 1, \dots, \ell-1$. +3. The resulting sparse grid has the same number of points as the Clenshaw-Curtis grid. + +The 1-D nodes at level $\ell$ are + +\[ +x_j^{(\ell)} = +\begin{cases} +\dfrac{1}{2} & \ell = 0 \\ +0,\;1 & \ell = 1 \\ +\dfrac{1}{2} - \dfrac{1}{2}\cos\!\left(\dfrac{(2j-1)\pi}{2^\ell}\right), +\quad j = 1, \dots, 2^{\ell-1} & \ell \geq 2 +\end{cases} +\] + +Barycentric Lagrange interpolation and the discrete cosine transform (DCT) are used +internally for efficient evaluation and surplus computation. + +--- + +## Gauss-Patterson grid + +Since version 5.0, the **Gauss-Patterson** sparse grid is available. It is based on the +abscissae of the Gauss-Patterson nested quadrature rule, which achieves a higher degree of +polynomial exactness than the CGL nodes for integration purposes. The maximum supported +depth is 6. + +The 1-D Gauss-Patterson nodes at level $\ell$ include $2^{\ell+1}-1$ nodes total (all +nodes from levels $0, \dots, \ell$ in a nested fashion). + +--- + +## Error estimate + +Define the function class + +\[ +\mathcal{F}_d^k = \left\{ + f : [0,1]^d \to \mathbb{R} + \;\middle|\; + \left\|\frac{\partial^{|\boldsymbol{\alpha}|} f}{\partial x^{\boldsymbol{\alpha}}}\right\|_\infty + \leq C_{\boldsymbol{\alpha}},\; + |\boldsymbol{\alpha}|_\infty \leq k +\right\} +\] + +i.e. all mixed partial derivatives up to order $k$ in each coordinate are bounded. For +$f \in \mathcal{F}_d^k$, the CGL sparse grid interpolant $A_{q,d}(f)$ satisfies + +\[ +\|f - A_{q,d}(f)\|_\infty += O\!\left(N^{-k}\,(\log N)^{(k+1)(d-1)}\right) +\] + +where $N$ is the number of CGL support nodes. Compared to the piecewise-linear rate +$N^{-2}(\log N)^{2(d-1)}$, the polynomial basis achieves the higher rate $N^{-k}$ for +smooth functions ($k > 2$). + +--- + +## Grid point counts + +The number of grid points of the **CGL grid** equals that of the Clenshaw-Curtis (CC) grid +(see [Linear Basis Functions](linear-basis.md)). + +The **Gauss-Patterson grid** has the same point counts as the NoBoundary (NB) grid. + +The figure below shows the CGL sparse grids at levels 0 and 2 in two and three dimensions. + +![CGL sparse grid, levels 0 and 2, d=2 and d=3](../_static/ex_polynomial_01.png) + +--- + +## When to use polynomial basis functions? + +There is a trade-off between accuracy gain and the computing time required both to construct +and to evaluate the interpolant. The higher-order accuracy only becomes effective with +increasing number of nodes. We recommend polynomial basis functions when **both** of the +following conditions are met: + +- The objective function is known to be **very smooth** (analytic or at least $C^k$ for + large $k$). +- **High relative accuracies** smaller than $10^{-2}$ are required. + +For lower accuracies or functions with limited smoothness, the piecewise-linear +Clenshaw-Curtis grid is generally the better choice. diff --git a/docs/usage/derivatives.md b/docs/usage/derivatives.md new file mode 100644 index 0000000..d312a3b --- /dev/null +++ b/docs/usage/derivatives.md @@ -0,0 +1,161 @@ +# Computing Derivatives + +One primary purpose of sparse grid interpolation is constructing **surrogate functions** for +local or global optimisation. `spinterp` computes exact derivatives of the interpolant — +not finite-difference approximations — at almost no additional memory cost. + +--- + +## How to obtain derivatives + +Call `spderiv_cc` (or `spderiv_cb` for the Chebyshev grid) instead of `spinterp_cc`. The +function returns both the interpolated values **and** the full gradient vector: + +```python +import spinterp + +# ip.shape = (npoints,) +# grad.shape = (npoints, d) +ip, grad = spinterp.spderiv_cc(z, pts_unit, seq) +``` + +The procedure for building the surpluses `z` and level-index matrix `seq` is identical +regardless of whether derivatives are needed. + +!!! note + The computed derivatives are the **exact** derivatives of the interpolant (up to + floating-point accuracy), not approximations of the derivatives of the original + function $f$. No additional memory is required. + +--- + +## Derivatives of piecewise multilinear interpolants + +Differentiating a piecewise linear function yields a **piecewise constant** function. The +derivatives are exact everywhere except at the kinks (the grid nodes), where only a +one-sided derivative exists. + +### Example — 2-D function + +Consider the test function + +\[ +f(x, y) = \frac{1}{\cos^2(2x) + \sin^2(y) + 1} + 0.2\,y +\] + +with exact partial derivatives + +\[ +\frac{\partial f}{\partial x} = + -\frac{4\cos(2x)\sin(2x)}{(\cos^2(2x) + \sin^2(y) + 1)^2}, +\qquad +\frac{\partial f}{\partial y} = + \frac{-2\cos(y)\sin(y)}{(\cos^2(2x) + \sin^2(y) + 1)^2} + 0.2. +\] + +The figure below compares $\partial f / \partial y$ (left, exact) with the sparse grid +derivative $\partial A^{\text{CC}}_{6,2}(f) / \partial y$ (right, piecewise constant with +visible jumps at level-4 grid nodes): + +![Exact vs piecewise-constant CC derivative](../_static/ex_spderiv_01.png) + +--- + +## Augmented continuous derivatives + +Discontinuous derivatives make first-order optimality conditions $\nabla f = \mathbf{0}$ +impossible to satisfy exactly, leading to slow convergence in gradient-based optimisation. + +The **continuous derivative** option linearly interpolates the piecewise-constant +derivative between two augmented evaluation points $y_1$ and $y_2$ on either side of each +grid cell: + +\[ +\frac{\partial A}{\partial x_k}\bigg|_\text{cont}(y) += \frac{\partial A}{\partial x_k}(y_1) + + \frac{\dfrac{\partial A}{\partial x_k}(y_2) - + \dfrac{\partial A}{\partial x_k}(y_1)}{\Delta} + (y - y_1) +\] + +where $\Delta$ is the cell width $1/2^{\ell_{\max}}$. + +Use `spcont_deriv_cc` followed by `pp_deriv` to obtain continuous derivatives: + +```python +maxlev = int(seq[:, 0].max()) +ip, ipder, ipder2 = spinterp.spcont_deriv_cc(z, pts_unit, seq, maxlev) +# pp_deriv post-processes ipder in-place using ipder2 +import numpy as np +maxlevvec = np.full(d, maxlev, dtype=np.int32) +spinterp.pp_deriv( + np.asfortranarray(ipder), + np.asfortranarray(ipder2), + maxlevvec, + np.asfortranarray(pts_unit) +) +``` + +The figure below shows the same derivative after the continuity post-processing: + +![Continuous CC derivative after pp_deriv post-processing](../_static/ex_spderiv_02.png) + +--- + +## Derivatives of polynomial interpolants + +For the Chebyshev-Gauss-Lobatto grid, the basis functions are globally smooth polynomials. +The derivatives are computed via the **discrete cosine transform (DCT)**, using the +`spderiv_cb` function: + +```python +ip, grad = spinterp.spderiv_cb(z_cb, pts_unit, seq) +``` + +The resulting derivatives are infinitely smooth: + +![CGL polynomial derivative vs exact](../_static/ex_spderiv_03.png) + +--- + +## Approximation quality + +The figure below shows the maximum absolute error of the derivatives for six standard +Genz test functions at 100 randomly sampled points for dimension $d = 3$: + +- **H$^\text{CC}$** — piecewise constant, Clenshaw-Curtis grid +- **H$^\text{CC}$ (cont.)** — augmented continuous, Clenshaw-Curtis grid +- **H$^\text{CGL}$** — smooth polynomial, Chebyshev grid + +![Derivative approximation quality comparison for d=3](../_static/ex_spderiv_04.png) + +!!! note + Functions with kinks (labelled *continuous* and *discontinuous*) cannot have their + derivatives approximated in the maximum norm: convergence fails near the kinks. + The error decreases in the plot only because the randomly sampled points are less + likely to land near the (shrinking) non-convergent region. + +--- + +## Computational cost + +### Clenshaw-Curtis + +Computing the exact or augmented continuous gradient adds only a small, dimension-independent +factor over plain interpolation: + +![Timing: exact CC derivative](../_static/timespderiv.png) +![Timing: continuous CC derivative](../_static/timespderiv_cont.png) + +### Chebyshev + +The polynomial case requires more sophisticated algorithms. However, as the dimension +increases, fewer subgrids need differentiation (lower-dimensional subgrids omit the +dimensions they do not span), so the overhead decreases: + +![Timing: CGL derivative](../_static/timespderiv_cheb.png) +![Timing: CGL derivative absolute](../_static/timespderiv_cheb_abs.png) + +For comparison, numerical differentiation with a centred-difference formula would require +$2d + 1$ interpolant evaluations per gradient — the analytic approach is substantially +cheaper for moderate to large $d$. diff --git a/docs/usage/index.md b/docs/usage/index.md new file mode 100644 index 0000000..391f25c --- /dev/null +++ b/docs/usage/index.md @@ -0,0 +1,10 @@ +# API Reference + +This section covers the main tasks you can perform with a sparse grid interpolant once it +has been constructed. + +| Page | Description | +|---|---| +| [Computing Derivatives](derivatives.md) | Exact gradients of the interpolant | +| [Numerical Integration](integration.md) | Quadrature via the sparse grid weights | +| [Improving Performance](performance.md) | Vectorisation, purging, reuse of results | diff --git a/docs/usage/integration.md b/docs/usage/integration.md new file mode 100644 index 0000000..6dca92b --- /dev/null +++ b/docs/usage/integration.md @@ -0,0 +1,109 @@ +# Numerical Integration (Quadrature) + +Once a sparse grid interpolant has been constructed, its integral over the domain can be +computed exactly (with respect to the interpolant) by taking the dot product of the +**quadrature weights** with the **hierarchical surpluses**. + +For the unit domain $[0,1]^d$: + +\[ +\int_{[0,1]^d} A_{n,d}(f)(\mathbf{x})\,\mathrm{d}\mathbf{x} += \sum_{\mathbf{l}} \sum_j w_j^{(\mathbf{l})}\, \alpha_j^{(\mathbf{l})} += \mathbf{w}^\top \mathbf{z} +\] + +where $\mathbf{w}$ is the vector of quadrature weights and $\mathbf{z}$ is the flat +surplus array. + +```python +import spinterp, numpy as np + +# Build quadrature weights for the CC grid +# (seq and z come from the standard surplus construction) +tp_all = sum(spinterp.spdim_cc(s) for s in all_seq) +w = spinterp.spquadw_cc(seq, tp_all) +integral = float(np.dot(w, z)) +``` + +--- + +## Integration of regular sparse grid interpolants + +### Test: product-type function in $d = 5$ + +Consider the integration test function + +\[ +f(\mathbf{x}) = \left(1 + \frac{1}{d}\right)^d \prod_{i=1}^{d} x_i^{1/d} +\] + +over $[0,1]^5$. The exact integral is $1$. The table below reproduces results from +Table 1 of *Gerstner & Griebel, Numerical Algorithms 18(3–4), 1998*: + +| Depth | CC points | CC error | Cheby points | Cheby error | GP points | GP error | +|---|---|---|---|---|---|---| +| 0 | 1 | 2.44e-01 | 1 | 2.44e-01 | 1 | 2.44e-01 | +| 1 | 11 | 1.08e+00 | 11 | 6.39e-01 | 11 | 8.94e-03 | +| 2 | 61 | 7.58e-02 | 61 | 1.44e-01 | 71 | 8.07e-04 | +| 3 | 241 | 2.86e-01 | 241 | 1.24e-01 | 351 | 2.07e-04 | +| 4 | 801 | 1.08e-01 | 801 | 6.65e-03 | 1471 | 2.26e-05 | +| 5 | 2433 | 8.00e-02 | 2433 | 1.06e-02 | 5503 | 1.42e-06 | +| 6 | 6993 | 5.03e-02 | 6993 | 1.74e-03 | 18943 | 3.44e-09 | + +!!! note + The grid types *Clenshaw-Curtis* and *Chebyshev* in this toolbox correspond to the + "Trapez rule" and "Clenshaw-Curtis rule" sparse grids in Gerstner-Griebel (1998), + respectively. + +--- + +## Integration of dimension-adaptive interpolants + +For high-dimensional problems, the dimension-adaptive approach can achieve dramatically +better accuracy with the same number of function evaluations compared to Monte Carlo. + +### Absorption problem ($d = 20$) + +Consider the absorption integral from Morokoff and Caflisch (1995). Using the smooth +representation of the integrand with parameters $\gamma = 0.5$, $x = 0$, $d = 20$, the +exact solution is + +\[ +I = \frac{1}{\gamma} - \frac{1-\gamma}{\gamma}\,e^{\gamma(1-x)} + \approx 0.3513. +\] + +The dimension-adaptive Chebyshev (CGL) sparse grid converges orders of magnitude faster +than crude Monte Carlo with the same number of points: + +| Points | CGL error | MC error (avg.) | +|---|---|---| +| 41 | 4.62e-04 | 1.30e-02 | +| 87 | 5.61e-06 | 6.01e-03 | +| 177 | 6.01e-07 | 7.79e-03 | +| 367 | 1.57e-07 | 3.44e-03 | +| 739 | 3.89e-08 | 4.76e-03 | +| 1531 | 2.46e-08 | 1.90e-03 | +| 3085 | 1.06e-09 | 1.04e-03 | +| 6181 | 2.75e-09 | 6.15e-04 | +| 24795 | 3.01e-10 | 5.42e-04 | +| 49739 | 1.79e-10 | 3.31e-04 | + +The improvement is particularly pronounced here because the smooth integrand (second +representation) is well-suited to polynomial approximation, and its near-additive structure +is exploited by the dimension-adaptive algorithm. + +--- + +## Quadrature weight functions + +| Grid type | Weight function | Notes | +|---|---|---| +| Clenshaw-Curtis | `spquadw_cc(seq, nw)` | Simple uniform weights $1/\text{wval}$ | +| Chebyshev | `spquadw_cb(seq, nw, w1d, startid)` | Needs 1-D table from `cheb_weights` | +| Gauss-Patterson | `spquadw_gp(seq, nw, w1d, startid)` | Needs 1-D table from `gp_weights` | +| Maximum | `spquadw_m(seq, nw)` | Explicit 1-D weight array per dim | +| NoBoundary | `spquadw_nb(seq, nw)` | Endpoint weights doubled | + +All weight functions also have sparse-index variants (`_sp` suffix) for high-dimensional +problems. diff --git a/docs/usage/performance.md b/docs/usage/performance.md new file mode 100644 index 0000000..ba26d0c --- /dev/null +++ b/docs/usage/performance.md @@ -0,0 +1,151 @@ +# Improving Performance + +## Overview + +This section covers the main strategies for making `spinterp` faster: + +1. Vectorising the objective function +2. Reusing previously computed results +3. Purging redundant interpolant data +4. Vectorised interpolant evaluation + +--- + +## Vectorising the objective function + +Vectorisation is most beneficial when function evaluations are cheap ($\lesssim 10^{-2}$ s +each). Consider the function + +\[ +f(x_1, x_2) = (x_1\, x_2)^2. +\] + +A non-vectorised implementation evaluates one point at a time: + +```python +def f(x1, x2): + return (x1 * x2) ** 2 +``` + +A vectorised implementation processes a whole array in one call: + +```python +import numpy as np +def f_vec(x1, x2): + return (x1 * x2) ** 2 # NumPy broadcasts automatically +``` + +When passing a vectorised function, the grid points are delivered as full arrays, avoiding +per-point Python call overhead. For cheap functions, this can give a **2×** or better +speed-up when computing surpluses. + +--- + +## Reusing previous results + +The hierarchical surplus structure means that a higher-level interpolant is a strict +extension of a lower-level one. You never need to recompute previously computed levels. + +The recommended pattern is to build the interpolant in a loop, checking the estimated error +after each level: + +```python +import numpy as np, spinterp + +d, n_max = 2, 8 +all_seq, all_surp = [], [] +est_err = np.inf + +for k in range(n_max + 1): + nl = spinterp.spnlevels(k, d) + seq = spinterp.spgetseq(k, d, nl) + tp = spinterp.spdim_cc(seq) + x_k = spinterp.spgrid_cc(seq, tp) + fvals = np.array([f_vec(*x_k[i]) for i in range(tp)]) + + if k == 0: + surp = fvals.copy() + else: + z_prev = np.concatenate(all_surp) + seq_prev = np.vstack(all_seq) + surp = fvals - spinterp.spcmpvals_cc(z_prev, x_k, seq, seq_prev) + + all_seq.append(seq) + all_surp.append(surp) + + # Estimated error = max |surplus| / (max |f| + eps) + est_err = np.max(np.abs(surp)) / (np.max(np.abs(fvals)) + 1e-14) + print(f"level {k}: {tp} new pts, est. rel. error = {est_err:.3e}") + + if est_err < 1e-4: + break +``` + +Example output for a smooth function: + +``` +level 0: 1 new pts, est. rel. error = 1.000e+00 +level 1: 4 new pts, est. rel. error = 3.750e-01 +level 2: 8 new pts, est. rel. error = 4.688e-02 +level 3: 16 new pts, est. rel. error = 2.930e-03 +level 4: 28 new pts, est. rel. error = 8.545e-05 +``` + +--- + +## Purging interpolant data + +After the interpolant is built, subgrids whose hierarchical surpluses are all below a +**drop tolerance** can be excluded from evaluation without significantly affecting accuracy. +This is the `sppurge` concept from the original MATLAB toolbox. + +A simple Python equivalent: keep only subgrids where $|\alpha_{\mathbf{l}}|_\infty$ exceeds +a threshold: + +```python +drop_tol = 1e-5 +keep = [ + (seq, surp) + for seq, surp in zip(all_seq, all_surp) + if np.max(np.abs(surp)) > drop_tol +] +seq_pruned = np.vstack([s for s, _ in keep]) +z_pruned = np.concatenate([surp for _, surp in keep]) +``` + +The trade-off: smaller `drop_tol` → higher accuracy, slower evaluation. Typical savings +are 2–5× speedup at the cost of $\sim 10^{-4}$ relative accuracy degradation when +`drop_tol = 1e-5`. + +![Purging trade-off: computation time vs maximum error](../_static/ex_performance_01.png) + +--- + +## Vectorised interpolant evaluation + +The `spinterp_cc` (and related) functions are designed for **batch evaluation**: passing +all query points as a single 2-D array is far more efficient than one-point-at-a-time +calls. + +```python +# Slow — Python loop over 1000 points +results = [spinterp.spinterp_cc(z, pts[i:i+1], seq)[0] for i in range(1000)] + +# Fast — single batched call +results = spinterp.spinterp_cc(z, pts, seq) # pts.shape = (1000, d) +``` + +Typical speed-up: **50–100×** for 1000 points, due to reduced Python overhead and better +cache utilisation in the Fortran kernel. + +--- + +## Summary of tips + +| Tip | When to apply | Typical benefit | +|---|---|---| +| Vectorise the objective function | Cheap functions ($< 10^{-2}$ s/eval) | 2–10× faster `spcmpvals` | +| Build levels incrementally | Always | Avoids recomputing surpluses | +| Purge small-surplus subgrids | After convergence, if evaluation speed matters | 2–5× faster `spinterp` | +| Batch query points | Always | 50–100× faster `spinterp` | +| Use sparse index format | $d > 10$ | Lower memory, faster iteration | diff --git a/meson.build b/meson.build index 144a461..7d8b812 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project( - '', + 'spinterp', 'fortran', 'c', version: run_command(['python', 'bin/get_version.py'], check: true).stdout().strip() ) diff --git a/mkdocs.yml b/mkdocs.yml index 365d8a5..f699be3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,41 +1,38 @@ -site_name: -site_description: -site_url: https://eggzec.github.io// +site_name: spinterp +site_description: Sparse Grid Interpolation Toolbox for Python +site_url: https://eggzec.github.io/spinterp/ docs_dir: docs site_dir: build/docs -repo_name: eggzec/ -repo_url: https://github.com/eggzec/ +repo_name: eggzec/spinterp +repo_url: https://github.com/eggzec/spinterp -# Copyright -copyright: Copyright © 2026 +copyright: "Copyright © 2026 eggzec. All Rights Reserved." -# Configuration +# --------------------------------------------------------------------------- +# Theme +# --------------------------------------------------------------------------- theme: name: material language: en features: - # content features - - content.code.copy # https://squidfunk.github.io/mkdocs-material/reference/code-blocks/?h=code+copy#code-copy-button - - content.code.annotate # https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#code-annotations - # navigation features - - navigation.instant # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=navigation+instant#instant-loading - - navigation.instant.progress # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=navigation+instant#progress-indicator - - navigation.tracking # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=navigation+instant#anchor-tracking - - navigation.tabs # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=navigation+instant#navigation-tabs - - navigation.tabs.sticky # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=navigation+instant#sticky-navigation-tabs - - navigation.sections # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=navigation+instant#navigation-sections - - navigation.indexes # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=navigation+instant#section-index-pages - - toc.follow # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=navigation+instant#anchor-following - - navigation.top # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=navigation+instant#back-to-top-button - - navigation.footer # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#navigation-footer - # search features - - search.suggest # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/?h=search+highlight#search-suggestions - - search.highlight # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/?h=search+highlight#search-highlighting - - search.share # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/?h=search+highlight#search-sharing + - content.code.copy + - content.code.annotate + - navigation.instant + - navigation.instant.progress + - navigation.tracking + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.indexes + - toc.follow + - navigation.top + - navigation.footer + - search.suggest + - search.highlight + - search.share - # Palette configuration palette: - scheme: default primary: blue grey @@ -50,93 +47,39 @@ theme: icon: material/brightness-4 name: Switch to light mode - # Logo and favicon - favicon: assets/images/.ico - logo: assets/images/.png - - # Font configuration font: text: Roboto code: Roboto Mono -# Navigation structure +# --------------------------------------------------------------------------- +# Navigation +# --------------------------------------------------------------------------- nav: - - Get Started: - - index.md - - Installation: installation.md - - Contributing: contributing.md - - License: license.md - - Credits: credits.md - - Contributors: contributors.md - - Users: users.md + - Home: index.md + - Installation: installation.md + - Quick Start: quickstart.md - Theory: - - Introduction: - - Overview: theory/introduction/index.md - - Experimental Design: theory/introduction/what-is-doe.md - - Uses of DOE: theory/introduction/doe-uses.md - - Steps of DOE: theory/introduction/doe-steps.md - - Assumptions: - - Overview: theory/assumptions/index.md - - Measurement System: theory/assumptions/measurement-system.md - - Process Stability: theory/assumptions/process-stability.md - - Simple Model: theory/assumptions/simple-model.md - - Residuals: theory/assumptions/residuals.md - - Choosing an Experimental Design: - - Overview: theory/choosing-design/index.md - - Set Objectives: theory/choosing-design/objectives.md - - Select Process Variables: theory/choosing-design/process-variables.md - - Select Experimental Design: theory/choosing-design/select-design.md - - Completely Randomized Designs: theory/choosing-design/completely-randomized.md - - Randomized Block Designs: theory/choosing-design/randomized-block.md - - Full Factorial Designs: theory/choosing-design/full-factorial.md - - Fractional Factorial Designs: theory/choosing-design/fractional-factorial.md - - Plackett-Burman Designs: theory/choosing-design/plackett-burman.md - - Response Surface Designs: theory/choosing-design/response-surface.md - - Central Composite Designs: theory/choosing-design/central-composite.md - - Box-Behnken Designs: theory/choosing-design/box-behnken.md - - Comparisons of Response Surface Designs: theory/choosing-design/design-comparisons.md - - Blocking a Response Surface Design: theory/choosing-design/blocking-rsd.md - - Adding Center Points: theory/choosing-design/center-points.md - - Improving Fractional Design Resolution: theory/choosing-design/improving-resolution.md - - Three-Level Full Factorial Designs: theory/choosing-design/three-level.md - - Three-Level, Mixed Level and Fractional Factorial Designs: theory/choosing-design/mixed-level.md - - Analysis of DOE Data: - - Overview: theory/analysis-doe/index.md - - DOE Analysis Steps: theory/analysis-doe/doe-analysis-steps.md - - Plotting DOE Data: theory/analysis-doe/plotting-doe-data.md - - Modeling DOE Data: theory/analysis-doe/modeling-doe-data.md - - Testing and Revising Models: theory/analysis-doe/testing-revising-models.md - - Interpreting DOE Results: theory/analysis-doe/interpreting-results.md - - Confirming DOE Results: theory/analysis-doe/confirming-results.md - - DOE Examples: theory/analysis-doe/doe-examples.md - - Full Factorial Example: theory/analysis-doe/full-factorial-example.md - - Fractional Factorial Example: theory/analysis-doe/fractional-factorial-example.md - - Response Surface Example: theory/analysis-doe/response-surface-example.md - - Advanced Topics: - - Overview: theory/advanced-topics/index.md - - When Classical Designs Don't Work: theory/advanced-topics/when-classical-fail.md - - Computer-Aided Designs: theory/advanced-topics/computer-aided-designs.md - - D-Optimal Designs: theory/advanced-topics/d-optimal.md - - Repairing a Design: theory/advanced-topics/repairing-design.md - - Optimizing a Process: theory/advanced-topics/optimizing-process.md - - Single Response Case: theory/advanced-topics/single-response.md - - Multiple Response Case: theory/advanced-topics/multiple-response.md - - Reference: - - Factorial Designs: reference/factorial.md - - Response Surface Designs: reference/response_surface.md - - Randomized Designs: reference/randomized.md - - Low-Discrepancy Sequences: reference/low_discrepancy_sequences.md - - Sampling Designs: reference/sampling_designs.md - - Taguchi Designs: reference/taguchi.md - - Optimal Designs: reference/doe_optimal.md - - Sparse Grid Designs: reference/sparse_grid.md - - Changelog: changelog.md + - Linear Basis Functions: theory/linear-basis.md + - Polynomial Basis Functions: theory/polynomial-basis.md + - Dimensional Adaptivity: theory/dimension-adaptive.md + - Bibliography: theory/bibliography.md + - API Reference: + - Overview: usage/index.md + - Computing Derivatives: usage/derivatives.md + - Numerical Integration: usage/integration.md + - Improving Performance: usage/performance.md +# --------------------------------------------------------------------------- +# Plugins +# --------------------------------------------------------------------------- plugins: - - search: # https://squidfunk.github.io/mkdocs-material/plugins/search - - minify: # https://github.com/byrnereese/mkdocs-minify-plugin + - search: + - minify: minify_html: true +# --------------------------------------------------------------------------- +# Markdown extensions +# --------------------------------------------------------------------------- markdown_extensions: - attr_list - md_in_html @@ -158,22 +101,22 @@ markdown_extensions: - pymdownx.arithmatex: generic: true -extra_css: # https://squidfunk.github.io/mkdocs-material/customization/#additional-css +extra_css: - assets/stylesheets/extra.css - - https://unpkg.com/katex@0/dist/katex.min.css extra: social: - icon: fontawesome/brands/github - link: https://github.com/eggzec/ + link: https://github.com/eggzec/spinterp - icon: fontawesome/brands/python - link: https://pypi.org/project// + link: https://pypi.org/project/spinterp/ extra_javascript: - - assets/javascripts/katex.js - - https://unpkg.com/katex@0/dist/katex.min.js - - https://unpkg.com/katex@0/dist/contrib/auto-render.min.js + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js +# --------------------------------------------------------------------------- +# Validation +# --------------------------------------------------------------------------- validation: nav: omitted_files: warn diff --git a/pyproject.toml b/pyproject.toml index 6a07625..ba3ac16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,10 +8,11 @@ requires = [ build-backend = "mesonpy" [project] -name = "" +name = "spinterp" dynamic = ["version"] -description = "" +description = "Sparse Grid Interpolation Toolbox for Python" authors = [ + { name = "W. Andreas Klimke", email = "klimkeas@ians.uni-stuttgart.de" }, { name = "Saud Zahir", email = "m.saud.zahir@gmail.com" }, ] maintainers = [ @@ -19,9 +20,20 @@ maintainers = [ { name = "M Laraib Ali", email = "laraibg786@outlook.com" } ] readme = "README.md" -license = "GPL-3.0" +license = {text = "GPL-3.0"} keywords = [ - "python" + "sparse grid", + "interpolation", + "quadrature", + "Smolyak", + "Clenshaw-Curtis", + "Chebyshev", + "hierarchical surplus", + "high-dimensional approximation", + "Fortran", + "f2py", + "scientific computing", + "numerical methods", ] classifiers = [ "Development Status :: 5 - Production/Stable", @@ -50,12 +62,12 @@ dependencies = [ requires-python = ">=3.10" [project.urls] -homepage = "https://eggzec.github.io//" -documentation = "https://eggzec.github.io//" -source = "https://github.com/eggzec/" -changelog = "https://eggzec.github.io//changelog/" -releasenotes = "https://github.com/eggzec//releases/latest" -issues = "https://github.com/eggzec//issues" +homepage = "https://eggzec.github.io/spinterp/" +documentation = "https://eggzec.github.io/spinterp/" +source = "https://github.com/eggzec/spinterp" +changelog = "https://eggzec.github.io/spinterp/changelog/" +releasenotes = "https://github.com/eggzec/spinterp/releases/latest" +issues = "https://github.com/eggzec/spinterp/issues" [dependency-groups] dev = [ @@ -79,7 +91,7 @@ test = [ source = "vcs" [tool.hatch.build.targets.wheel] -include = [ "/" ] +include = [ "/spinterp" ] [tool.ruff] line-length = 80 diff --git a/src/barypdstepcb.f b/src/barypdstepcb.f new file mode 100644 index 0000000..ac31158 --- /dev/null +++ b/src/barypdstepcb.f @@ -0,0 +1,287 @@ +C ******************************************************************* +C +C BARY_PD_STEP_CB - Barycentric d-dim Lagrange interpolation step, +C Chebyshev grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE BARY_PD_STEP_CB(Z, NZ, ALLNX, DIMS, D, + & X, NX, Y, NINTERP, DYFULL, IP) +C ******************************************************************* +C +C BARY_PD_STEP_CB evaluates the multi-dimensional polynomial +C interpolant using barycentric Lagrange interpolation on the +C Chebyshev-Lobatto grid. Only the NZ new (hierarchical) nodes +C contribute; the node structure has NX(k) total nodes per dim, +C with new nodes at even positions (0,2,4,...). +C +C Parameters: +C +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - number of new nodes +C Input, INTEGER ALLNX(D) - full node count per dim +C Input, INTEGER DIMS(D) - active dimension indices +C Input, INTEGER D - active dimensions +C Input, DOUBLE PRECISION X(NX) - concatenated node arrays +C Input, INTEGER NX - total nodes in X +C Input, DOUBLE PRECISION Y(NINTERP,DYFULL) - query points +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER DYFULL - full dimension of Y +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NZ, D, NX, NINTERP, DYFULL + DOUBLE PRECISION Z(NZ) + INTEGER ALLNX(D), DIMS(D) + DOUBLE PRECISION X(NX) + DOUBLE PRECISION Y(NINTERP, DYFULL) + DOUBLE PRECISION IP(NINTERP) + + INTEGER K, L, K2, KXI, AID, XID, ACTNX, ACTNA + INTEGER NNA(50), IDADDFIX(50), AIDSTART(50), AIDMAX(50) + INTEGER AIDVEC(50), IDVEC(50), IDADD(50) + INTEGER NID, NITER + INTEGER ZIDBASE, ZID, ID(50) + INTEGER AID1, AID2, AIDMAX1, AIDMAX2, ADDID1, ADDID2 + INTEGER ID1, ID2, IDL, AIDL, AIDLMAX + DOUBLE PRECISION A(16384) + DOUBLE PRECISION C1(50), C2(50), S, FRAC, T + DOUBLE PRECISION C1D, C2D, C11, C12 + INTEGER ISZERONODE + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + END DO + +C NNA(k) = number of new (non-zero) nodes per dim k +C For level 1 (ALLNX=3): NNA = 2; else NNA = (ALLNX-1)/2 + DO L = 1, D + IF (ALLNX(L) .EQ. 3) THEN + NNA(L) = 2 + ELSE + NNA(L) = (ALLNX(L) - 1) / 2 + END IF + END DO + +C Column strides in Z (new-node layout) + IDADDFIX(1) = 1 + AIDSTART(1) = 1 + DO L = 2, D + IDADDFIX(L) = IDADDFIX(L-1) * NNA(L-1) + AIDSTART(L) = AIDSTART(L-1) + NNA(L-1) + END DO + DO L = 1, D + AIDMAX(L) = AIDSTART(L) + NNA(L) - 1 + END DO + + DO K = 1, NINTERP + XID = 0 + AID = 0 + ISZERONODE = 0 + + DO L = 1, D + T = Y(K, DIMS(L)) + S = 0.0D+00 + ID(L) = 0 + ACTNX = ALLNX(L) + ACTNA = NNA(L) + + IF (ACTNX .EQ. 3) THEN +C Level 1: 3 nodes (x[1]=0, x[2]=0.5, x[3]=1) +C x[2] (index 2, zero node) sets ip=0 if hit + IF (T .EQ. X(XID+1)) THEN + ID(L) = 1 + C2(L) = 1.0D+00 + AID = AID + ACTNA + XID = XID + ACTNX + GOTO 30 + ELSE IF (T .EQ. X(XID+2)) THEN + ISZERONODE = 1 + GOTO 50 + ELSE IF (T .EQ. X(XID+3)) THEN + ID(L) = 2 + C2(L) = 1.0D+00 + AID = AID + ACTNA + XID = XID + ACTNX + GOTO 30 + END IF + FRAC = 0.5D+00 / (T - X(XID+1)) + A(AID+1) = FRAC + S = S + FRAC + S = S - 1.0D+00 / (T - X(XID+2)) + FRAC = 0.5D+00 / (T - X(XID+3)) + A(AID+2) = FRAC + S = S + FRAC + + ELSE +C General level >= 2 +C Check if t hits a support node (even pos) +C or a zero node (odd pos) using acos approximation + IF (T .GE. 0.0D+00 .AND. T .LE. 1.0D+00) THEN + K2 = XID + NINT(DBLE(ACTNX) - DACOS( + & (T-0.5D+00)*2.0D+00) + & *DBLE(ACTNX-1)/ + & 3.14159265358979323846D+00) + IF (K2 .LT. XID+1) K2 = XID+1 + IF (K2 .GT. XID+ACTNX) K2 = XID+ACTNX + IF (T .EQ. X(K2)) THEN + IF (MOD(K2-XID, 2) .EQ. 1) THEN + ISZERONODE = 1 + GOTO 50 + ELSE + ID(L) = (K2-XID)/2 + C2(L) = 1.0D+00 + AID = AID + ACTNA + XID = XID + ACTNX + GOTO 30 + END IF + END IF + END IF + + S = S + 0.5D+00 / (T - X(XID+1)) + KXI = XID + 2 + DO K2 = AID+1, AID+ACTNA-1 + FRAC = -1.0D+00 / (T - X(KXI)) + A(K2) = FRAC + S = S + FRAC + 1.0D+00/(T - X(KXI+1)) + KXI = KXI + 2 + END DO + S = S - 0.5D+00 / (T - X(XID+ACTNX)) + END IF + + C2(L) = S + AID = AID + ACTNA + XID = XID + ACTNX + 30 CONTINUE + END DO + + IF (ISZERONODE .NE. 0) GOTO 40 + +C Determine free (non-hit) dimensions + NITER = 1 + NID = 0 + DO L = 1, D + IF (ID(L) .EQ. 0) THEN + NITER = NITER * NNA(L) + NID = NID + 1 + IDVEC(NID) = L + ELSE + ID(L) = ID(L) - 1 + END IF + END DO + + IF (NID .EQ. 0) THEN + ZIDBASE = 1 + DO L = 1, D + ZIDBASE = ZIDBASE + ID(L)*IDADDFIX(L) + END DO + IP(K) = Z(ZIDBASE) + GOTO 40 + END IF + + ZIDBASE = 1 + DO L = 1, D + C1(L) = 0.0D+00 + ZIDBASE = ZIDBASE + ID(L)*IDADDFIX(L) + AIDVEC(L) = AIDSTART(L) + END DO + + DO L = 1, NID + IDL = IDVEC(L) + IF (L .GT. 1) THEN + IDADD(IDL) = IDADDFIX(IDL) + & - IDADDFIX(IDVEC(L-1)+1) + ELSE + IDADD(IDL) = IDADDFIX(IDL) + END IF + END DO + + C11 = 0.0D+00 + C12 = 0.0D+00 + ID1 = IDVEC(1) + ADDID1 = IDADD(ID1) + AID1 = AIDVEC(ID1) + AIDMAX1 = AIDMAX(ID1) + ZID = ZIDBASE + + IF (NID .EQ. 1) THEN + DO K2 = AID1, AIDMAX1 + C11 = C11 + Z(ZID) * A(K2) + ZID = ZID + ADDID1 + END DO + C1D = C11 + + ELSE + NITER = NITER / NNA(ID1) + ID2 = IDVEC(2) + AID2 = AIDVEC(ID2) + AIDMAX2 = AIDMAX(ID2) + ADDID2 = IDADD(ID2) + + DO K2 = 1, NITER + DO AID = AID1, AIDMAX1 + C11 = C11 + Z(ZID) * A(AID) + ZID = ZID + ADDID1 + END DO + ZID = ZID + ADDID2 + C12 = C12 + A(AID2) * C11 + C11 = 0.0D+00 + AID2 = AID2 + 1 + + IF (NID .GT. 2 .AND. AID2 .GT. AIDMAX2) THEN + ZID = ZID + IDADD(IDVEC(3)) + C1(3) = C1(3) + A(AIDVEC(IDVEC(3)))*C12 + C12 = 0.0D+00 + AID2 = AIDSTART(ID2) + DO L = 3, NID + IDL = IDVEC(L) + AIDL = AIDVEC(IDL) + AIDLMAX = AIDMAX(IDL) + IF (AIDL .EQ. AIDLMAX) THEN + IF (L .LT. NID) THEN + ZID = ZID + & + IDADD(IDVEC(L+1)) + C1(L+1) = C1(L+1) + & + A(AIDVEC(IDVEC(L+1))) + & * C1(L) + C1(L) = 0.0D+00 + AIDVEC(IDL) = + & AIDSTART(IDL) + END IF + ELSE + AIDVEC(IDL) = AIDL + 1 + GOTO 60 + END IF + END DO + 60 CONTINUE + END IF + END DO + + IF (NID .EQ. 2) THEN + C1D = C12 + ELSE + C1D = C1(NID) + END IF + END IF + + C2D = 1.0D+00 + DO L = 1, D + C2D = C2D * C2(L) + END DO + IP(K) = C1D / C2D + + 40 CONTINUE + END DO + + 50 RETURN + END diff --git a/src/barypdstepgp.f b/src/barypdstepgp.f new file mode 100644 index 0000000..7abc6d9 --- /dev/null +++ b/src/barypdstepgp.f @@ -0,0 +1,295 @@ +C ******************************************************************* +C +C BARY_PD_STEP_GP - Barycentric d-dim Lagrange interpolation step, +C Gauss-Patterson grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE BARY_PD_STEP_GP(Z, NZ, ALLNX, DIMS, D, + & X, NX, Y, NINTERP, DYFULL, + & W, IP) +C ******************************************************************* +C +C BARY_PD_STEP_GP evaluates the multi-dimensional polynomial +C interpolant using barycentric Lagrange interpolation on the +C Gauss-Patterson grid. Includes precomputed barycentric +C weights W in addition to node locations X. +C +C Parameters: +C +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - number of new nodes +C Input, INTEGER ALLNX(D) - full node count per dim +C Input, INTEGER DIMS(D) - active dimension indices +C Input, INTEGER D - active dimensions +C Input, DOUBLE PRECISION X(NX) - concatenated node arrays +C Input, INTEGER NX - total nodes in X +C Input, DOUBLE PRECISION Y(NINTERP,DYFULL) - query points +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER DYFULL - full dimension of Y +C Input, DOUBLE PRECISION W(NX) - barycentric weights +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NZ, D, NX, NINTERP, DYFULL + DOUBLE PRECISION Z(NZ) + INTEGER ALLNX(D), DIMS(D) + DOUBLE PRECISION X(NX), W(NX) + DOUBLE PRECISION Y(NINTERP, DYFULL) + DOUBLE PRECISION IP(NINTERP) + + INTEGER K, L, K2, KXI, AID, XID, ACTNX, ACTNA + INTEGER NNA(50), IDADDFIX(50), AIDSTART(50), AIDMAX(50) + INTEGER AIDVEC(50), IDVEC(50), IDADD(50) + INTEGER NID, NITER + INTEGER ZIDBASE, ZID, ID(50) + INTEGER AID1, AID2, AIDMAX1, AIDMAX2, ADDID1, ADDID2 + INTEGER ID1, ID2, IDL, AIDL, AIDLMAX + INTEGER LOK, UPK, KMID + DOUBLE PRECISION A(16384) + DOUBLE PRECISION C1(50), C2(50), S, FRAC, T, XK + DOUBLE PRECISION C1D, C2D, C11, C12 + INTEGER ISZERONODE + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + END DO + +C NNA(k) = (ALLNX(k) + 1) / 2 (new GP nodes per dim) + DO L = 1, D + NNA(L) = (ALLNX(L) + 1) / 2 + END DO + + IDADDFIX(1) = 1 + AIDSTART(1) = 1 + DO L = 2, D + IDADDFIX(L) = IDADDFIX(L-1) * NNA(L-1) + AIDSTART(L) = AIDSTART(L-1) + NNA(L-1) + END DO + DO L = 1, D + AIDMAX(L) = AIDSTART(L) + NNA(L) - 1 + END DO + + DO K = 1, NINTERP + XID = 0 + AID = 0 + ISZERONODE = 0 + + DO L = 1, D + T = Y(K, DIMS(L)) + S = 0.0D+00 + ID(L) = 0 + ACTNX = ALLNX(L) + ACTNA = NNA(L) + + IF (ACTNX .EQ. 3) THEN + IF (T .EQ. X(XID+1)) THEN + ID(L) = 1 + C2(L) = 1.0D+00 + AID = AID + ACTNA + XID = XID + ACTNX + GOTO 30 + ELSE IF (T .EQ. X(XID+2)) THEN + ISZERONODE = 1 + GOTO 50 + ELSE IF (T .EQ. X(XID+3)) THEN + ID(L) = 2 + C2(L) = 1.0D+00 + AID = AID + ACTNA + XID = XID + ACTNX + GOTO 30 + END IF + FRAC = W(XID+1) / (T - X(XID+1)) + A(AID+1) = FRAC + S = S + FRAC + S = S + W(XID+2) / (T - X(XID+2)) + FRAC = W(XID+3) / (T - X(XID+3)) + A(AID+2) = FRAC + S = S + FRAC + + ELSE +C Binary search for t in the node array + IF (T .GE. 0.0D+00 .AND. T .LE. 1.0D+00) THEN + LOK = XID + 1 + UPK = XID + ACTNX + KMID = (LOK + UPK) / 2 + XK = X(KMID) + 70 IF (LOK + 1 .LT. UPK) THEN + IF (XK .GT. T) THEN + UPK = KMID + ELSE + LOK = KMID + END IF + KMID = (LOK + UPK) / 2 + XK = X(KMID) + GOTO 70 + END IF + IF (T .EQ. X(LOK)) THEN + K2 = LOK + ELSE IF (T .EQ. X(UPK)) THEN + K2 = UPK + ELSE + K2 = 0 + END IF + IF (K2 .GT. 0) THEN + IF (MOD(K2-XID, 2) .EQ. 0) THEN + ISZERONODE = 1 + GOTO 50 + ELSE + ID(L) = (K2-XID+1)/2 + C2(L) = 1.0D+00 + AID = AID + ACTNA + XID = XID + ACTNX + GOTO 30 + END IF + END IF + END IF + + KXI = XID + 1 + DO K2 = AID+1, AID+ACTNA-1 + FRAC = W(KXI) / (T - X(KXI)) + A(K2) = FRAC + S = S + FRAC + & + W(KXI+1)/(T - X(KXI+1)) + KXI = KXI + 2 + END DO + FRAC = W(KXI) / (T - X(KXI)) + A(AID+ACTNA) = FRAC + S = S + FRAC + END IF + + C2(L) = S + AID = AID + ACTNA + XID = XID + ACTNX + 30 CONTINUE + END DO + + IF (ISZERONODE .NE. 0) GOTO 40 + + NITER = 1 + NID = 0 + DO L = 1, D + IF (ID(L) .EQ. 0) THEN + NITER = NITER * NNA(L) + NID = NID + 1 + IDVEC(NID) = L + ELSE + ID(L) = ID(L) - 1 + END IF + END DO + + IF (NID .EQ. 0) THEN + ZIDBASE = 1 + DO L = 1, D + ZIDBASE = ZIDBASE + ID(L)*IDADDFIX(L) + END DO + IP(K) = Z(ZIDBASE) + GOTO 40 + END IF + + ZIDBASE = 1 + DO L = 1, D + C1(L) = 0.0D+00 + ZIDBASE = ZIDBASE + ID(L)*IDADDFIX(L) + AIDVEC(L) = AIDSTART(L) + END DO + + DO L = 1, NID + IDL = IDVEC(L) + IF (L .GT. 1) THEN + IDADD(IDL) = IDADDFIX(IDL) + & - IDADDFIX(IDVEC(L-1)+1) + ELSE + IDADD(IDL) = IDADDFIX(IDL) + END IF + END DO + + C11 = 0.0D+00 + C12 = 0.0D+00 + ID1 = IDVEC(1) + ADDID1 = IDADD(ID1) + AID1 = AIDVEC(ID1) + AIDMAX1 = AIDMAX(ID1) + ZID = ZIDBASE + + IF (NID .EQ. 1) THEN + DO K2 = AID1, AIDMAX1 + C11 = C11 + Z(ZID) * A(K2) + ZID = ZID + ADDID1 + END DO + C1D = C11 + + ELSE + NITER = NITER / NNA(ID1) + ID2 = IDVEC(2) + AID2 = AIDVEC(ID2) + AIDMAX2 = AIDMAX(ID2) + ADDID2 = IDADD(ID2) + + DO K2 = 1, NITER + DO AID = AID1, AIDMAX1 + C11 = C11 + Z(ZID) * A(AID) + ZID = ZID + ADDID1 + END DO + ZID = ZID + ADDID2 + C12 = C12 + A(AID2) * C11 + C11 = 0.0D+00 + AID2 = AID2 + 1 + + IF (NID .GT. 2 .AND. AID2 .GT. AIDMAX2) THEN + ZID = ZID + IDADD(IDVEC(3)) + C1(3) = C1(3) + A(AIDVEC(IDVEC(3)))*C12 + C12 = 0.0D+00 + AID2 = AIDSTART(ID2) + DO L = 3, NID + IDL = IDVEC(L) + AIDL = AIDVEC(IDL) + AIDLMAX = AIDMAX(IDL) + IF (AIDL .EQ. AIDLMAX) THEN + IF (L .LT. NID) THEN + ZID = ZID + & + IDADD(IDVEC(L+1)) + C1(L+1) = C1(L+1) + & + A(AIDVEC(IDVEC(L+1))) + & * C1(L) + C1(L) = 0.0D+00 + AIDVEC(IDL) = + & AIDSTART(IDL) + END IF + ELSE + AIDVEC(IDL) = AIDL + 1 + GOTO 60 + END IF + END DO + 60 CONTINUE + END IF + END DO + + IF (NID .EQ. 2) THEN + C1D = C12 + ELSE + C1D = C1(NID) + END IF + END IF + + C2D = 1.0D+00 + DO L = 1, D + C2D = C2D * C2(L) + END DO + IP(K) = C1D / C2D + + 40 CONTINUE + END DO + + 50 RETURN + END diff --git a/src/chebweights.f b/src/chebweights.f new file mode 100644 index 0000000..0bac119 --- /dev/null +++ b/src/chebweights.f @@ -0,0 +1,112 @@ +C ******************************************************************* +C +C CHEB_WEIGHTS - 1-D Chebyshev-Lobatto quadrature weights. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE CHEB_WEIGHTS(MAXLEV, WEIGHTS, NW, STARTID) +C ******************************************************************* +C +C CHEB_WEIGHTS builds the flat 1-D weight array and level start +C indices for SPQUADW_CB, mirroring MATLAB chebweights.m. +C +C Parameters: +C +C Input, INTEGER MAXLEV - max Chebyshev level +C Output, DOUBLE PRECISION WEIGHTS(NW) - flat weight array +C Input, INTEGER NW - size of WEIGHTS +C Output, INTEGER STARTID(MAXLEV+1) - level start indices +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER MAXLEV, NW + DOUBLE PRECISION WEIGHTS(NW) + INTEGER STARTID(*) + + INTEGER LEV, I, WID, NW1 + DOUBLE PRECISION W(512) + + WID = 0 + + DO LEV = 0, MAXLEV + + IF (LEV .EQ. 0) THEN + W(1) = 1.0D+00 + NW1 = 1 + ELSE IF (LEV .EQ. 1) THEN + W(1) = 1.0D+00 / 6.0D+00 + NW1 = 1 + ELSE IF (LEV .EQ. 2) THEN + W(1) = 4.0D+00 / 15.0D+00 + NW1 = 1 + ELSE IF (LEV .EQ. 3) THEN + W(1) = 0.7310932460800910D-01 + W(2) = 0.1808589293602449D+00 + NW1 = 2 + ELSE IF (LEV .EQ. 4) THEN + W(1) = 0.1868435141860280D-01 + W(2) = 0.5445277629094545D-01 + W(3) = 0.8158633214085165D-01 + W(4) = 0.9625693230646280D-01 + NW1 = 4 + ELSE IF (LEV .EQ. 5) THEN + W(1) = 0.4696598981477508D-02 + W(2) = 0.1422895833861684D-01 + W(3) = 0.2313138141887588D-01 + W(4) = 0.3113605477264700D-01 + W(5) = 0.3794190022069424D-01 + W(6) = 0.4328876922091372D-01 + W(7) = 0.4697162221938437D-01 + W(8) = 0.4884909410402779D-01 + NW1 = 8 + ELSE IF (LEV .EQ. 6) THEN + W( 1) = 0.1175745337655852D-02 + W( 2) = 0.3596346580868057D-02 + W( 3) = 0.5961697357106385D-02 + W( 4) = 0.8267493828644795D-02 + W( 5) = 0.1049313721486872D-01 + W( 6) = 0.1261753249087738D-01 + W( 7) = 0.1462032659873417D-01 + W( 8) = 0.1648227328498816D-01 + W( 9) = 0.1818546014331959D-01 + W(10) = 0.1971349435647805D-01 + W(11) = 0.2105166555570905D-01 + W(12) = 0.2218708961962866D-01 + W(13) = 0.2310883375546279D-01 + W(14) = 0.2380802229262510D-01 + W(15) = 0.2427792242857052D-01 + W(16) = 0.2451400921551278D-01 + NW1 = 16 + ELSE + NW1 = 2**(LEV-2) + DO I = 1, NW1 + W(I) = 0.0D+00 + END DO + END IF + + STARTID(LEV+1) = WID + 1 + DO I = 1, NW1 + WEIGHTS(WID + I) = W(I) + END DO + WID = WID + NW1 + + IF (LEV .GT. 0) THEN + DO I = 1, NW1 + WEIGHTS(WID + I) = W(NW1 - I + 1) + END DO + WID = WID + NW1 + END IF + + END DO + + RETURN + END diff --git a/src/dctdiffcheb.f b/src/dctdiffcheb.f new file mode 100644 index 0000000..f60044d --- /dev/null +++ b/src/dctdiffcheb.f @@ -0,0 +1,160 @@ +C ******************************************************************* +C +C DCT_DIFF_CHEB - DCT-based derivative of Chebyshev polynomial. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE DCT_DIFF_CHEB(N, Z, NPTS, NCOLS, Y, NY, IPDER) +C ******************************************************************* +C +C DCT_DIFF_CHEB computes the derivative of the Chebyshev polynomial +C at the new nodes using DCT-based coefficient extraction. +C +C N = full Chebyshev node count (2^lev + 1) +C NPTS = new surplus values only: lev=1 -> 2, lev>1 -> (N-1)/2 +C NCOLS = 1 (univariate) or NY (one column per query point) +C +C Algorithm (MATLAB dctdiffcheb): +C n==3: ZEXT = [Z(2), 0, Z(1), 0] +C n>3: ZEXT(2i) = Z(NPTS+1-i) i=1..NPTS (reversed) +C ZEXT(2NPTS+2i) = Z(i) i=1..NPTS (direct) +C Then DFT -> Chebyshev coeffs -> differentiate -> Clenshaw eval. +C +C Parameters: +C +C Input, INTEGER N - full Chebyshev node count +C Input, DOUBLE PRECISION Z(NPTS,NCOLS) - new surplus values +C Input, INTEGER NPTS - number of new surplus values +C Input, INTEGER NCOLS - 1 (univariate) or NY +C Input, DOUBLE PRECISION Y(NY) - eval points in [0,1] +C Input, INTEGER NY - number of eval points +C Output, DOUBLE PRECISION IPDER(NY) - accumulated derivative +C +C ******************************************************************* + + IMPLICIT NONE + + DOUBLE PRECISION PI + PARAMETER (PI = 3.14159265358979323846D+00) + + INTEGER N, NPTS, NCOLS, NY + DOUBLE PRECISION Z(NPTS, NCOLS) + DOUBLE PRECISION Y(NY) + DOUBLE PRECISION IPDER(NY) + + INTEGER MAXM + PARAMETER (MAXM = 2050) + + INTEGER M, I, J, K, COL + DOUBLE PRECISION ZEXT(MAXM) + DOUBLE PRECISION CHEBCOEF(MAXM) + DOUBLE PRECISION DCHEB(MAXM) + DOUBLE PRECISION ANGLE, T, B0, B1, B2, SUMRE + LOGICAL UNIVARIATE + + DO K = 1, NY + IPDER(K) = 0.0D+00 + END DO + + IF (N .LE. 1 .OR. NPTS .LE. 0) RETURN + + UNIVARIATE = (NCOLS .EQ. 1) + M = 2 * (N - 1) + + DO COL = 1, NCOLS + + DO I = 1, M + ZEXT(I) = 0.0D+00 + END DO + + IF (N .EQ. 3) THEN +C Special: new nodes at x=0 (Z(1)) and x=1 (Z(2)) +C ZEXT = [Z(2), 0, Z(1), 0] + IF (NPTS .GE. 2) THEN + ZEXT(1) = Z(2, COL) + ZEXT(3) = Z(1, COL) + ELSE + ZEXT(1) = Z(1, COL) + END IF + ELSE +C ZEXT(2i) = Z(NPTS+1-i) for i=1..NPTS +C ZEXT(2*NPTS + 2i) = Z(i) for i=1..NPTS + DO I = 1, NPTS + ZEXT(2*I) = Z(NPTS + 1 - I, COL) + END DO + DO I = 1, NPTS + IF (2*NPTS + 2*I .LE. M) + & ZEXT(2*NPTS + 2*I) = Z(I, COL) + END DO + END IF + +C DFT of ZEXT -> Chebyshev coefficients + DO K = 0, N - 1 + SUMRE = 0.0D+00 + DO J = 0, M - 1 + ANGLE = -2.0D+00*PI*DBLE(J)*DBLE(K)/DBLE(M) + SUMRE = SUMRE + ZEXT(J+1) * DCOS(ANGLE) + END DO + CHEBCOEF(K+1) = SUMRE / DBLE(N-1) + END DO + CHEBCOEF(1) = CHEBCOEF(1) / 2.0D+00 + CHEBCOEF(N) = CHEBCOEF(N) / 2.0D+00 + +C Differentiate Chebyshev coefficients (MATLAB backward recurrence) + DO K = 1, N + DCHEB(K) = 0.0D+00 + END DO + IF (N .GE. 3) THEN + DCHEB(N-1) = 2.0D+00*DBLE(N-1)*CHEBCOEF(N) + DO K = N - 2, 2, -1 + DCHEB(K) = DCHEB(K+2) + & + 2.0D+00*DBLE(K)*CHEBCOEF(K+1) + END DO + DCHEB(1) = DCHEB(3)/2.0D+00 + CHEBCOEF(2) + ELSE IF (N .EQ. 2) THEN + DCHEB(1) = CHEBCOEF(2) + END IF + +C Scale by 2 for [0,1] domain + DO K = 1, N - 1 + DCHEB(K) = DCHEB(K) * 2.0D+00 + END DO + +C Clenshaw evaluation + IF (UNIVARIATE) THEN + DO I = 1, NY + T = 2.0D+00 * Y(I) - 1.0D+00 + B0 = 0.0D+00 + B1 = 0.0D+00 + B2 = 0.0D+00 + DO K = N - 1, 2, -1 + B2 = B1 + B1 = B0 + B0 = DCHEB(K) + 2.0D+00*T*B1 - B2 + END DO + IPDER(I) = IPDER(I) + DCHEB(1) + T*B0 - B1 + END DO + ELSE + T = 2.0D+00 * Y(COL) - 1.0D+00 + B0 = 0.0D+00 + B1 = 0.0D+00 + B2 = 0.0D+00 + DO K = N - 1, 2, -1 + B2 = B1 + B1 = B0 + B0 = DCHEB(K) + 2.0D+00*T*B1 - B2 + END DO + IPDER(COL) = IPDER(COL) + DCHEB(1) + T*B0 - B1 + END IF + + END DO + + RETURN + END diff --git a/src/dctupsample.f b/src/dctupsample.f new file mode 100644 index 0000000..ada9e14 --- /dev/null +++ b/src/dctupsample.f @@ -0,0 +1,126 @@ +C ******************************************************************* +C +C DCT_UPSAMPLE - Upsample 1D grid data to finer CC grid using DCT. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE DCT_UPSAMPLE(Z, NZ, NCOLS, NNEW, IP, NOUT) +C ******************************************************************* +C +C DCT_UPSAMPLE upsamples 1D grid data from N nodes to NNEW nodes +C using DCT (Chebyshev extension) and returns only the even-indexed +C new nodes (the new hierarchical nodes at the finer level). +C +C Algorithm: +C 1. Extend Z: ZEXT = [Z; flip(Z(2:N-1))] length 2*(N-1) +C 2. FFT of ZEXT +C 3. Scale: chebc = FFT[1:N] / (N-1) * (NNEW-1) +C 4. Half the N-th coefficient +C 5. Pad with zeros to length 2*(NNEW-1) +C 6. IFFT +C 7. Return even-indexed values: ip = ipext(2,4,...,NNEW) +C +C NOTE: DFT/IDFT implemented as O(N^2) for correctness. +C For production use FFTPACK or similar. +C +C Parameters: +C +C Input, DOUBLE PRECISION Z(NZ,NCOLS) - input grid values (N rows) +C Input, INTEGER NZ - rows of Z (=N) +C Input, INTEGER NCOLS - columns of Z +C Input, INTEGER NNEW - size of new full grid +C Output, DOUBLE PRECISION IP(NOUT,NCOLS) - upsampled new nodes +C Input, INTEGER NOUT - output rows = (NNEW-1)/2 +C +C ******************************************************************* + + IMPLICIT NONE + + DOUBLE PRECISION PI + PARAMETER (PI = 3.14159265358979323846D+00) + + INTEGER NZ, NCOLS, NNEW, NOUT + DOUBLE PRECISION Z(NZ, NCOLS) + DOUBLE PRECISION IP(NOUT, NCOLS) + + INTEGER MAXM + PARAMETER (MAXM = 1024) + + INTEGER N, M, MN, I, J, K, COL + DOUBLE PRECISION ZEXT(MAXM) + DOUBLE PRECISION CREAL(MAXM), CIMAG(MAXM) + DOUBLE PRECISION CHEBC_R(MAXM), CHEBC_I(MAXM) + DOUBLE PRECISION IPEXT_R(MAXM), IPEXT_I(MAXM) + DOUBLE PRECISION ANGLE, SCALE_IN, SCALE_OUT, SUMRE, SUMIM + + N = NZ + M = 2 * (N - 1) + MN = 2 * (NNEW - 1) + + DO COL = 1, NCOLS + +C Build extended array + DO I = 1, N + ZEXT(I) = Z(I, COL) + END DO + DO I = 2, N - 1 + ZEXT(M - I + 2) = Z(I, COL) + END DO + +C Forward DFT of ZEXT length M + SCALE_IN = 1.0D+00 / DBLE(N-1) + DO K = 0, N - 1 + SUMRE = 0.0D+00 + SUMIM = 0.0D+00 + DO J = 0, M - 1 + ANGLE = -2.0D+00*PI*DBLE(J)*DBLE(K)/DBLE(M) + SUMRE = SUMRE + ZEXT(J+1) * DCOS(ANGLE) + SUMIM = SUMIM + ZEXT(J+1) * DSIN(ANGLE) + END DO + CHEBC_R(K+1) = SUMRE * SCALE_IN * DBLE(NNEW-1) + CHEBC_I(K+1) = SUMIM * SCALE_IN * DBLE(NNEW-1) + END DO + +C Halve the N-th coefficient (index N, 1-based) + CHEBC_R(N) = CHEBC_R(N) / 2.0D+00 + CHEBC_I(N) = CHEBC_I(N) / 2.0D+00 + +C Pad CHEBC to length MN with zeros + DO K = 1, N + CREAL(K) = CHEBC_R(K) + CIMAG(K) = CHEBC_I(K) + END DO + DO K = N + 1, MN + CREAL(K) = 0.0D+00 + CIMAG(K) = 0.0D+00 + END DO + +C Inverse DFT of length MN + SCALE_OUT = 1.0D+00 / DBLE(MN) + DO I = 0, MN - 1 + SUMRE = 0.0D+00 + DO K = 0, MN - 1 + ANGLE = 2.0D+00*PI*DBLE(K)*DBLE(I)/DBLE(MN) + SUMRE = SUMRE + CREAL(K+1)*DCOS(ANGLE) + & - CIMAG(K+1)*DSIN(ANGLE) + END DO + IPEXT_R(I+1) = SUMRE * SCALE_OUT + END DO + +C Return even-indexed values: 2, 4, ..., NNEW (1-based) +C i.e. ipext(2), ipext(4), ..., ipext(NNEW) + DO I = 1, NOUT + IP(I, COL) = IPEXT_R(2*I) + END DO + + END DO + + RETURN + END diff --git a/src/getchebnodes.f b/src/getchebnodes.f new file mode 100644 index 0000000..217ebf5 --- /dev/null +++ b/src/getchebnodes.f @@ -0,0 +1,54 @@ +C ******************************************************************* +C +C GET_CHEB_NODES - Chebyshev nodes for barycentric interpolation. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE GET_CHEB_NODES(ALLNX, D, X, NX) +C ******************************************************************* +C +C GET_CHEB_NODES builds a flat array of Chebyshev-Lobatto nodes +C for each of the D dimensions with ALLNX(k) nodes in dim k. +C +C Node formula for dimension k with n = ALLNX(k) nodes: +C x_i = 0.5 - cos(i*pi/(n-1)) / 2, i = 0,...,n-1 +C +C Parameters: +C +C Input, INTEGER ALLNX(D) - node counts per dimension +C Input, INTEGER D - number of dimensions +C Output, DOUBLE PRECISION X(NX) - concatenated node array +C Input, INTEGER NX - total nodes = sum(ALLNX) +C +C ******************************************************************* + + IMPLICIT NONE + + DOUBLE PRECISION PI + PARAMETER (PI = 3.14159265358979323846D+00) + + INTEGER D, NX + INTEGER ALLNX(D) + DOUBLE PRECISION X(NX) + + INTEGER K, I, AID, N + + AID = 0 + DO K = 1, D + N = ALLNX(K) + DO I = 0, N-1 + X(AID+I+1) = 0.5D+00 + & - DCOS(DBLE(I)*PI/DBLE(N-1)) / 2.0D+00 + END DO + AID = AID + N + END DO + + RETURN + END diff --git a/src/getgpbaryw.f b/src/getgpbaryw.f new file mode 100644 index 0000000..d6f1a33 --- /dev/null +++ b/src/getgpbaryw.f @@ -0,0 +1,50 @@ +C ******************************************************************* +C +C GET_GP_BARY_W - Gauss-Patterson barycentric weights array. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE GET_GP_BARY_W(ALLNX, D, W, NW) +C ******************************************************************* +C +C GET_GP_BARY_W builds a flat array of GP barycentric weights for +C each of the D dimensions. ALLNX(k) = 2^(lev+1)-1 for dim k. +C +C Parameters: +C +C Input, INTEGER ALLNX(D) - full node counts per dim +C Input, INTEGER D - number of dimensions +C Output, DOUBLE PRECISION W(NW) - concatenated weight array +C Input, INTEGER NW - total weights = sum(ALLNX) +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NW + INTEGER ALLNX(D) + DOUBLE PRECISION W(NW) + + INTEGER K, AID, N, LEV + + AID = 0 + DO K = 1, D + N = ALLNX(K) + LEV = 0 + 10 IF (2**(LEV+1) - 1 .LT. N) THEN + LEV = LEV + 1 + GOTO 10 + END IF + CALL GP_BARY_W(LEV, W(AID+1), N) + AID = AID + N + END DO + + RETURN + END diff --git a/src/getgpnodes.f b/src/getgpnodes.f new file mode 100644 index 0000000..25c1277 --- /dev/null +++ b/src/getgpnodes.f @@ -0,0 +1,51 @@ +C ******************************************************************* +C +C GET_GP_NODES - Gauss-Patterson nodes for barycentric interp. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE GET_GP_NODES(ALLNX, D, X, NX) +C ******************************************************************* +C +C GET_GP_NODES builds a flat array of Gauss-Patterson nodes for +C each of the D dimensions. ALLNX(k) = 2^(lev+1)-1 for dim k. +C +C Parameters: +C +C Input, INTEGER ALLNX(D) - full node counts per dim +C Input, INTEGER D - number of dimensions +C Output, DOUBLE PRECISION X(NX) - concatenated node array +C Input, INTEGER NX - total nodes = sum(ALLNX) +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NX + INTEGER ALLNX(D) + DOUBLE PRECISION X(NX) + + INTEGER K, AID, N, LEV + + AID = 0 + DO K = 1, D + N = ALLNX(K) +C Level = log2(N+1) - 1 + LEV = 0 + 10 IF (2**(LEV+1) - 1 .LT. N) THEN + LEV = LEV + 1 + GOTO 10 + END IF + CALL GP_ABSC(LEV, X(AID+1), N) + AID = AID + N + END DO + + RETURN + END diff --git a/src/gpabsc.f b/src/gpabsc.f new file mode 100644 index 0000000..d320076 --- /dev/null +++ b/src/gpabsc.f @@ -0,0 +1,200 @@ +C ******************************************************************* +C +C GP_ABSC - Gauss-Patterson abscissae (nodes on [0,1]). +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE GP_ABSC(LEVEL, X, NX) +C ******************************************************************* +C +C GP_ABSC returns the Gauss-Patterson nodes on [0,1] for the given +C LEVEL (0..6). NX = 2^(LEVEL+1) - 1 is the number of nodes. +C +C The raw data (symmetric half on [-1,1]) is stored with the +C largest (outermost) value first. Mirror + shift maps to [0,1]: +C x_i = 0.5 + raw(i) / 2 for i = 1,...,NX +C +C Parameters: +C +C Input, INTEGER LEVEL - GP level (0..6) +C Output, DOUBLE PRECISION X(NX) - node array +C Input, INTEGER NX - number of nodes = 2^(LEVEL+1)-1 +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER LEVEL, NX + DOUBLE PRECISION X(NX) + + INTEGER I, NHALF + DOUBLE PRECISION RAW(64) + + IF (LEVEL .EQ. 0) THEN + X(1) = 0.5D+00 + RETURN + END IF + + IF (LEVEL .EQ. 1) THEN + RAW(1) = 0.77459666924148337704D+00 + RAW(2) = 0.00000000000000000000D+00 + NHALF = 2 + ELSE IF (LEVEL .EQ. 2) THEN + RAW(1) = 0.96049126870802028342D+00 + RAW(2) = 0.77459666924148337704D+00 + RAW(3) = 0.43424374934680255800D+00 + RAW(4) = 0.00000000000000000000D+00 + NHALF = 4 + ELSE IF (LEVEL .EQ. 3) THEN + RAW(1) = 0.99383196321275502221D+00 + RAW(2) = 0.96049126870802028342D+00 + RAW(3) = 0.88845923287225699889D+00 + RAW(4) = 0.77459666924148337704D+00 + RAW(5) = 0.62110294673722640294D+00 + RAW(6) = 0.43424374934680255800D+00 + RAW(7) = 0.22338668642896688163D+00 + RAW(8) = 0.00000000000000000000D+00 + NHALF = 8 + ELSE IF (LEVEL .EQ. 4) THEN + RAW( 1) = 0.99909812496766759766D+00 + RAW( 2) = 0.99383196321275502221D+00 + RAW( 3) = 0.98153114955374010687D+00 + RAW( 4) = 0.96049126870802028342D+00 + RAW( 5) = 0.92965485742974005667D+00 + RAW( 6) = 0.88845923287225699889D+00 + RAW( 7) = 0.83672593816886873550D+00 + RAW( 8) = 0.77459666924148337704D+00 + RAW( 9) = 0.70249620649152707861D+00 + RAW(10) = 0.62110294673722640294D+00 + RAW(11) = 0.53131974364437562397D+00 + RAW(12) = 0.43424374934680255800D+00 + RAW(13) = 0.33113539325797683309D+00 + RAW(14) = 0.22338668642896688163D+00 + RAW(15) = 0.11248894313318662575D+00 + RAW(16) = 0.00000000000000000000D+00 + NHALF = 16 + ELSE IF (LEVEL .EQ. 5) THEN + RAW( 1) = 0.99987288812035761194D+00 + RAW( 2) = 0.99909812496766759766D+00 + RAW( 3) = 0.99720625937222195908D+00 + RAW( 4) = 0.99383196321275502221D+00 + RAW( 5) = 0.98868475754742947994D+00 + RAW( 6) = 0.98153114955374010687D+00 + RAW( 7) = 0.97218287474858179658D+00 + RAW( 8) = 0.96049126870802028342D+00 + RAW( 9) = 0.94634285837340290515D+00 + RAW(10) = 0.92965485742974005667D+00 + RAW(11) = 0.91037115695700429250D+00 + RAW(12) = 0.88845923287225699889D+00 + RAW(13) = 0.86390793819369047715D+00 + RAW(14) = 0.83672593816886873550D+00 + RAW(15) = 0.80694053195021761186D+00 + RAW(16) = 0.77459666924148337704D+00 + RAW(17) = 0.73975604435269475868D+00 + RAW(18) = 0.70249620649152707861D+00 + RAW(19) = 0.66290966002478059546D+00 + RAW(20) = 0.62110294673722640294D+00 + RAW(21) = 0.57719571005204581484D+00 + RAW(22) = 0.53131974364437562397D+00 + RAW(23) = 0.48361802694584102756D+00 + RAW(24) = 0.43424374934680255800D+00 + RAW(25) = 0.38335932419873034692D+00 + RAW(26) = 0.33113539325797683309D+00 + RAW(27) = 0.27774982202182431507D+00 + RAW(28) = 0.22338668642896688163D+00 + RAW(29) = 0.16823525155220746498D+00 + RAW(30) = 0.11248894313318662575D+00 + RAW(31) = 0.05634431304659278997D+00 + RAW(32) = 0.00000000000000000000D+00 + NHALF = 32 + ELSE IF (LEVEL .EQ. 6) THEN + RAW( 1) = 0.99998243035489159858D+00 + RAW( 2) = 0.99987288812035761194D+00 + RAW( 3) = 0.99959879967191068325D+00 + RAW( 4) = 0.99909812496766759766D+00 + RAW( 5) = 0.99831663531840739253D+00 + RAW( 6) = 0.99720625937222195908D+00 + RAW( 7) = 0.99572410469840718851D+00 + RAW( 8) = 0.99383196321275502221D+00 + RAW( 9) = 0.99149572117810613240D+00 + RAW(10) = 0.98868475754742947994D+00 + RAW(11) = 0.98537149959852037111D+00 + RAW(12) = 0.98153114955374010687D+00 + RAW(13) = 0.97714151463970571416D+00 + RAW(14) = 0.97218287474858179658D+00 + RAW(15) = 0.96663785155841656709D+00 + RAW(16) = 0.96049126870802028342D+00 + RAW(17) = 0.95373000642576113641D+00 + RAW(18) = 0.94634285837340290515D+00 + RAW(19) = 0.93832039777959288365D+00 + RAW(20) = 0.92965485742974005667D+00 + RAW(21) = 0.92034002547001242073D+00 + RAW(22) = 0.91037115695700429250D+00 + RAW(23) = 0.89974489977694003664D+00 + RAW(24) = 0.88845923287225699889D+00 + RAW(25) = 0.87651341448470526974D+00 + RAW(26) = 0.86390793819369047715D+00 + RAW(27) = 0.85064449476835027976D+00 + RAW(28) = 0.83672593816886873550D+00 + RAW(29) = 0.82215625436498040737D+00 + RAW(30) = 0.80694053195021761186D+00 + RAW(31) = 0.79108493379984836143D+00 + RAW(32) = 0.77459666924148337704D+00 + RAW(33) = 0.75748396638051363793D+00 + RAW(34) = 0.73975604435269475868D+00 + RAW(35) = 0.72142308537009891548D+00 + RAW(36) = 0.70249620649152707861D+00 + RAW(37) = 0.68298743109107922809D+00 + RAW(38) = 0.66290966002478059546D+00 + RAW(39) = 0.64227664250975951377D+00 + RAW(40) = 0.62110294673722640294D+00 + RAW(41) = 0.59940393024224289297D+00 + RAW(42) = 0.57719571005204581484D+00 + RAW(43) = 0.55449513263193254887D+00 + RAW(44) = 0.53131974364437562397D+00 + RAW(45) = 0.50768775753371660215D+00 + RAW(46) = 0.48361802694584102756D+00 + RAW(47) = 0.45913001198983233287D+00 + RAW(48) = 0.43424374934680255800D+00 + RAW(49) = 0.40897982122988867241D+00 + RAW(50) = 0.38335932419873034692D+00 + RAW(51) = 0.35740383783153215238D+00 + RAW(52) = 0.33113539325797683309D+00 + RAW(53) = 0.30457644155671404334D+00 + RAW(54) = 0.27774982202182431507D+00 + RAW(55) = 0.25067873030348317661D+00 + RAW(56) = 0.22338668642896688163D+00 + RAW(57) = 0.19589750271110015392D+00 + RAW(58) = 0.16823525155220746498D+00 + RAW(59) = 0.14042423315256017459D+00 + RAW(60) = 0.11248894313318662575D+00 + RAW(61) = 0.08445404008371088371D+00 + RAW(62) = 0.05634431304659278997D+00 + RAW(63) = 0.02818464894974569434D+00 + RAW(64) = 0.00000000000000000000D+00 + NHALF = 64 + ELSE + DO I = 1, NX + X(I) = 0.5D+00 + END DO + RETURN + END IF + +C Mirror: full set = [-raw(1:NHALF-1), +raw(NHALF:-1:1)] mapped to [0,1] +C NX = 2*NHALF - 1 for level >= 1 + DO I = 1, NHALF - 1 + X(I) = 0.5D+00 - RAW(I) / 2.0D+00 + END DO + DO I = NHALF, NX + X(I) = 0.5D+00 + RAW(NX - I + 1) / 2.0D+00 + END DO + + RETURN + END diff --git a/src/gpbaryw.f b/src/gpbaryw.f new file mode 100644 index 0000000..4d728a8 --- /dev/null +++ b/src/gpbaryw.f @@ -0,0 +1,195 @@ +C ******************************************************************* +C +C GP_BARY_W - Gauss-Patterson barycentric weights. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE GP_BARY_W(LEVEL, W, NW) +C ******************************************************************* +C +C GP_BARY_W returns the Gauss-Patterson barycentric weights for +C the given LEVEL (0..6). NW = 2^(LEVEL+1)-1 weights. +C +C Parameters: +C +C Input, INTEGER LEVEL - GP level (0..6) +C Output, DOUBLE PRECISION W(NW) - barycentric weight array +C Input, INTEGER NW - number of weights +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER LEVEL, NW + DOUBLE PRECISION W(NW) + + INTEGER I, NHALF + DOUBLE PRECISION RAW(64) + + IF (LEVEL .EQ. 0) THEN + W(1) = 1.0D+00 + RETURN + END IF + + IF (LEVEL .EQ. 1) THEN + RAW(1) = 0.5000000000000000D+00 + RAW(2) =-1.0000000000000000D+00 + NHALF = 2 + ELSE IF (LEVEL .EQ. 2) THEN + RAW(1) = 0.2389562399804650D+00 + RAW(2) =-0.6554465493910690D+00 + RAW(3) = 0.9164903094106040D+00 + RAW(4) =-1.0000000000000000D+00 + NHALF = 4 + ELSE IF (LEVEL .EQ. 3) THEN + RAW(1) = 0.3085732742889269D+00 + RAW(2) =-0.7759609054034582D+00 + RAW(3) = 0.9730577082723834D+00 + RAW(4) =-1.0000000000000000D+00 + RAW(5) = 0.9651128023399624D+00 + RAW(6) =-0.9214581385020298D+00 + RAW(7) = 0.8904181282668750D+00 + RAW(8) =-0.8794857385253193D+00 + NHALF = 8 + ELSE IF (LEVEL .EQ. 4) THEN + RAW( 1) = 0.4284765107713552D+00 + RAW( 2) =-0.9674952645753376D+00 + RAW( 3) = 1.0000000000000000D+00 + RAW( 4) =-0.8073763738936263D+00 + RAW( 5) = 0.5959019645796662D+00 + RAW( 6) =-0.4284109973194455D+00 + RAW( 7) = 0.3090233900161985D+00 + RAW( 8) =-0.2271083041792955D+00 + RAW( 9) = 0.1715268309161644D+00 + RAW(10) =-0.1338120870473934D+00 + RAW(11) = 0.1081546364527703D+00 + RAW(12) =-0.9073420363174436D-01 + RAW(13) = 0.7909270443820152D-01 + RAW(14) =-0.7168121786085341D-01 + RAW(15) = 0.6756488645338050D-01 + RAW(16) =-0.6624495024008108D-01 + NHALF = 16 + ELSE IF (LEVEL .EQ. 5) THEN + RAW( 1) = 0.4761689774882791D+00 + RAW( 2) =-1.0000000000000000D+00 + RAW( 3) = 0.8951802743821745D+00 + RAW( 4) =-0.5931880330727972D+00 + RAW( 5) = 0.3446713216950697D+00 + RAW( 6) =-0.1887366139903847D+00 + RAW( 7) = 0.1010026343422441D+00 + RAW( 8) =-0.5391510866030504D-01 + RAW( 9) = 0.2905688513918218D-01 + RAW(10) =-0.1592647206003192D-01 + RAW(11) = 0.8917516424034839D-02 + RAW(12) =-0.5114369427222019D-02 + RAW(13) = 0.3009406140647174D-02 + RAW(14) =-0.1818655851124444D-02 + RAW(15) = 0.1129456765407580D-02 + RAW(16) =-0.7211000286455359D-03 + RAW(17) = 0.4733817579545664D-03 + RAW(18) =-0.3195580143057075D-03 + RAW(19) = 0.2218227922218166D-03 + RAW(20) =-0.1583257717017353D-03 + RAW(21) = 0.1161821456470950D-03 + RAW(22) =-0.8764186196347213D-04 + RAW(23) = 0.6795226274872346D-04 + RAW(24) =-0.5414390533152548D-04 + RAW(25) = 0.4432843387685334D-04 + RAW(26) =-0.3728544273524615D-04 + RAW(27) = 0.3221531510574336D-04 + RAW(28) =-0.2858909101688675D-04 + RAW(29) = 0.2605612252662254D-04 + RAW(30) =-0.2438687098702864D-04 + RAW(31) = 0.2343764915145207D-04 + RAW(32) =-0.2312961543917948D-04 + NHALF = 32 + ELSE IF (LEVEL .EQ. 6) THEN + RAW( 1) = 0.4961729559598435D+00 + RAW( 2) =-1.0000000000000000D+00 + RAW( 3) = 0.8207381753150933D+00 + RAW( 4) =-0.4792004570535736D+00 + RAW( 5) = 0.2370422415813909D+00 + RAW( 6) =-0.1072561730738105D+00 + RAW( 7) = 0.4621060128445129D-01 + RAW( 8) =-0.1940652396239318D-01 + RAW( 9) = 0.8059646108315423D-02 + RAW(10) =-0.3341041271341492D-02 + RAW(11) = 0.1391076421696200D-02 + RAW(12) =-0.5842685337623964D-03 + RAW(13) = 0.2483284595990911D-03 + RAW(14) =-0.1070501794628850D-03 + RAW(15) = 0.4688369444739026D-04 + RAW(16) =-0.2088588637335537D-04 + RAW(17) = 0.9472111162631427D-05 + RAW(18) =-0.4375709775264854D-05 + RAW(19) = 0.2059732032056890D-05 + RAW(20) =-0.9881358613753917D-06 + RAW(21) = 0.4831683085525125D-06 + RAW(22) =-0.2407971733363551D-06 + RAW(23) = 0.1223052268446143D-06 + RAW(24) =-0.6330402849816564D-07 + RAW(25) = 0.3338496881971787D-07 + RAW(26) =-0.1793649137605078D-07 + RAW(27) = 0.9815684853054276D-08 + RAW(28) =-0.5470515716389574D-08 + RAW(29) = 0.3104474727365682D-08 + RAW(30) =-0.1793611788188222D-08 + RAW(31) = 0.1054820854605286D-08 + RAW(32) =-0.6313496351815102D-09 + RAW(33) = 0.3845341465065858D-09 + RAW(34) =-0.2382913467980216D-09 + RAW(35) = 0.1502195793889654D-09 + RAW(36) =-0.9632258977165985D-10 + RAW(37) = 0.6281356365121918D-10 + RAW(38) =-0.4165284046151541D-10 + RAW(39) = 0.2808315197701900D-10 + RAW(40) =-0.1924877719136859D-10 + RAW(41) = 0.1341110141171783D-10 + RAW(42) =-0.9496850740372480D-11 + RAW(43) = 0.6834400742017730D-11 + RAW(44) =-0.4997820893808907D-11 + RAW(45) = 0.3713437304234225D-11 + RAW(46) =-0.2803137557864015D-11 + RAW(47) = 0.2149539400208833D-11 + RAW(48) =-0.1674329496665498D-11 + RAW(49) = 0.1324630884002739D-11 + RAW(50) =-0.1064322552770007D-11 + RAW(51) = 0.8684488054281593D-12 + RAW(52) =-0.7195782254680809D-12 + RAW(53) = 0.6054064647792446D-12 + RAW(54) =-0.5171611918326179D-12 + RAW(55) = 0.4485298932818066D-12 + RAW(56) =-0.3949320660460756D-12 + RAW(57) = 0.3530206289342299D-12 + RAW(58) =-0.3203375146310410D-12 + RAW(59) = 0.2950741104537000D-12 + RAW(60) =-0.2759038562229908D-12 + RAW(61) = 0.2618651977225959D-12 + RAW(62) =-0.2522803113493657D-12 + RAW(63) = 0.2466998748736465D-12 + RAW(64) =-0.2448675056241474D-12 + NHALF = 64 + ELSE + DO I = 1, NW + W(I) = 0.0D+00 + END DO + RETURN + END IF + +C Mirror: W = [raw(1:NHALF-1), fliplr(raw(1:NHALF))] + DO I = 1, NHALF - 1 + W(I) = RAW(I) + END DO + DO I = NHALF, NW + W(I) = RAW(NW - I + 1) + END DO + + RETURN + END diff --git a/src/gpweights.f b/src/gpweights.f new file mode 100644 index 0000000..00ca888 --- /dev/null +++ b/src/gpweights.f @@ -0,0 +1,151 @@ +C ******************************************************************* +C +C GP_WEIGHTS - 1-D Gauss-Patterson quadrature weights. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE GP_WEIGHTS(MAXLEV, WEIGHTS, NW, STARTID) +C ******************************************************************* +C +C GP_WEIGHTS builds the flat 1-D weight array and level start +C indices for SPQUADW_GP, mirroring MATLAB gpweights.m. +C +C For each level: weights include both new and mirrored half. +C All weights are divided by 2 (range [0,1] vs [-1,1]). +C +C Parameters: +C +C Input, INTEGER MAXLEV - max GP level (0..6) +C Output, DOUBLE PRECISION WEIGHTS(NW) - flat weight array +C Input, INTEGER NW - size of WEIGHTS +C Output, INTEGER STARTID(MAXLEV+1) - level start indices +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER MAXLEV, NW + DOUBLE PRECISION WEIGHTS(NW) + INTEGER STARTID(*) + + INTEGER LEV, I, WID, NW1 + DOUBLE PRECISION W(64) + + WID = 0 + + DO LEV = 0, MAXLEV + + IF (LEV .EQ. 0) THEN + W(1) = 2.0D+00 + NW1 = 1 + ELSE IF (LEV .EQ. 1) THEN + W(1) = 0.555555555555555555556D+00 + NW1 = 1 + ELSE IF (LEV .EQ. 2) THEN + W(1) = 0.104656226026467265194D+00 + W(2) = 0.401397414775962222905D+00 + NW1 = 2 + ELSE IF (LEV .EQ. 3) THEN + W(1) = 0.170017196299402603390D-01 + W(2) = 0.929271953151245376859D-01 + W(3) = 0.171511909136391380787D+00 + W(4) = 0.219156858401587496404D+00 + NW1 = 4 + ELSE IF (LEV .EQ. 4) THEN + W(1) = 0.254478079156187441540D-02 + W(2) = 0.164460498543878109338D-01 + W(3) = 0.359571033071293220968D-01 + W(4) = 0.569795094941233574122D-01 + W(5) = 0.768796204990035310427D-01 + W(6) = 0.936271099812644736167D-01 + W(7) = 0.105669893580234809744D+00 + W(8) = 0.111956873020953456880D+00 + NW1 = 8 + ELSE IF (LEV .EQ. 5) THEN + W(1) = 0.363221481845530659694D-03 + W(2) = 0.257904979468568827243D-02 + W(3) = 0.611550682211724633968D-02 + W(4) = 0.104982469096213218983D-01 + W(5) = 0.154067504665594978021D-01 + W(6) = 0.205942339159127111492D-01 + W(7) = 0.258696793272147469108D-01 + W(8) = 0.310735511116879648799D-01 + W(9) = 0.360644327807825726401D-01 + W(10) = 0.407155101169443189339D-01 + W(11) = 0.449145316536321974143D-01 + W(12) = 0.485643304066731987159D-01 + W(13) = 0.515832539520484587768D-01 + W(14) = 0.539054993352660639269D-01 + W(15) = 0.554814043565593639878D-01 + W(16) = 0.562776998312543012726D-01 + NW1 = 16 + ELSE IF (LEV .EQ. 6) THEN + W(1) = 0.505360952078625176247D-04 + W(2) = 0.377746646326984660274D-03 + W(3) = 0.938369848542381500794D-03 + W(4) = 0.168114286542146990631D-02 + W(5) = 0.256876494379402037313D-02 + W(6) = 0.357289278351729964938D-02 + W(7) = 0.467105037211432174741D-02 + W(8) = 0.584344987583563950756D-02 + W(9) = 0.707248999543355546805D-02 + W(10) = 0.834283875396815770558D-02 + W(11) = 0.964117772970253669530D-02 + W(12) = 0.109557333878379016480D-01 + W(13) = 0.122758305600827700870D-01 + W(14) = 0.135915710097655467896D-01 + W(15) = 0.148936416648151820348D-01 + W(16) = 0.161732187295777199419D-01 + W(17) = 0.174219301594641737472D-01 + W(18) = 0.186318482561387901863D-01 + W(19) = 0.197954950480974994880D-01 + W(20) = 0.209058514458120238522D-01 + W(21) = 0.219563663053178249393D-01 + W(22) = 0.229409642293877487608D-01 + W(23) = 0.238540521060385400804D-01 + W(24) = 0.246905247444876769091D-01 + W(25) = 0.254457699654647658126D-01 + W(26) = 0.261156733767060976805D-01 + W(27) = 0.266966229274503599062D-01 + W(28) = 0.271855132296247918192D-01 + W(29) = 0.275797495664818730349D-01 + W(30) = 0.278772514766137016085D-01 + W(31) = 0.280764557938172466068D-01 + W(32) = 0.281763190330166021307D-01 + NW1 = 32 + ELSE + NW1 = 2**(LEV-2) + DO I = 1, NW1 + W(I) = 0.0D+00 + END DO + END IF + +C Divide by 2 for [0,1] range + DO I = 1, NW1 + W(I) = W(I) / 2.0D+00 + END DO + + STARTID(LEV+1) = WID + 1 + DO I = 1, NW1 + WEIGHTS(WID + I) = W(I) + END DO + WID = WID + NW1 + + IF (LEV .GT. 0) THEN + DO I = 1, NW1 + WEIGHTS(WID + I) = W(NW1 - I + 1) + END DO + WID = WID + NW1 + END IF + + END DO + + RETURN + END diff --git a/src/meson.build b/src/meson.build index 66d0296..1887eb0 100644 --- a/src/meson.build +++ b/src/meson.build @@ -22,7 +22,7 @@ fortranobject_c = run_command(py, check: true ).stdout().strip() -py_mod_name = '' +py_mod_name = 'spinterp' fortran_sources = run_command(py, ['-c', 'import glob; print(" ".join(glob.glob("*.f")))' @@ -35,7 +35,7 @@ python_sources = run_command(py, ['-c', message('Python sources: ', python_sources) pyf_sources = [ - '.pyf' + 'spinterp.pyf' ] message('Fortran sources: ', fortran_sources + pyf_sources) diff --git a/src/nchoosek.f b/src/nchoosek.f new file mode 100644 index 0000000..d0d8a64 --- /dev/null +++ b/src/nchoosek.f @@ -0,0 +1,39 @@ +C ******************************************************************* +C +C NCHOOSEK - Binomial coefficient C(N, K). +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + INTEGER FUNCTION NCHOOSEK(N, K) +C ******************************************************************* +C +C NCHOOSEK computes the binomial coefficient C(N, K). +C +C Parameters: +C +C Input, INTEGER N - total elements +C Input, INTEGER K - chosen elements +C +C Return, INTEGER NCHOOSEK - binomial coefficient +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER N, K, I, RESULT + + RESULT = 1 + DO I = 0, K - 1 + RESULT = RESULT * (N - I) / (I + 1) + END DO + NCHOOSEK = RESULT + + RETURN + END diff --git a/src/popheap.f b/src/popheap.f new file mode 100644 index 0000000..494b13a --- /dev/null +++ b/src/popheap.f @@ -0,0 +1,74 @@ +C ******************************************************************* +C +C POP_HEAP - Pop max from max-heap and return its index. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE POP_HEAP(A, NA, G, NG, INDEX) +C ******************************************************************* +C +C POP_HEAP removes the max element from the heap (A(1)), replacing +C it with A(NA), then sifts down to restore the heap property. +C Returns the popped index in INDEX. +C +C Caller must set A(NA) to the replacement value before calling, +C and decrement NA after the call. +C +C Parameters: +C +C In/out, INTEGER A(NA) - heap indices (A(NA) = replacement) +C Input, INTEGER NA - current heap size +C Input, DOUBLE PRECISION G(NG) - priority values +C Input, INTEGER NG - length of G +C Output, INTEGER INDEX - popped index (old A(1)) +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NA, NG + INTEGER A(NA) + DOUBLE PRECISION G(NG) + INTEGER INDEX + + INTEGER K, LEFT, RIGHT, LARGEST, TMP + + INDEX = A(1) + A(1) = A(NA) + + K = 1 + + 10 CONTINUE + LEFT = 2 * K + RIGHT = 2 * K + 1 + +C Find largest among k, left, right (within NA-1 elements) + LARGEST = K + IF (LEFT .LE. NA - 1) THEN + IF (G(A(LEFT)) .GT. G(A(LARGEST))) THEN + LARGEST = LEFT + END IF + END IF + IF (RIGHT .LE. NA - 1) THEN + IF (G(A(RIGHT)) .GT. G(A(LARGEST))) THEN + LARGEST = RIGHT + END IF + END IF + + IF (LARGEST .NE. K) THEN + TMP = A(K) + A(K) = A(LARGEST) + A(LARGEST) = TMP + K = LARGEST + GOTO 10 + END IF + + RETURN + END diff --git a/src/ppderiv.f b/src/ppderiv.f new file mode 100644 index 0000000..9213309 --- /dev/null +++ b/src/ppderiv.f @@ -0,0 +1,102 @@ +C ******************************************************************* +C +C PP_DERIV - Post-processing for continuous derivatives. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE PP_DERIV(IPDER, IPDER2, NINTERP, D, + & MAXLEVVEC, Y) +C ******************************************************************* +C +C PP_DERIV applies post-processing to produce continuously +C differentiable derivatives by blending IPDER and IPDER2. +C +C For each point k and dimension l: +C - Compute ytd1 (cell center) and stepsize from maxlev. +C - If ipd1 * ipd2 >= 0: linear interpolation between boundaries. +C - Else if yt <= ytd1 + halfstep: decay from ipd1 toward zero. +C - Else: ramp from zero toward ipd2. +C +C Parameters: +C +C In/out, DOUBLE PRECISION IPDER(NINTERP,D) - gradient (modified) +C Input, DOUBLE PRECISION IPDER2(NINTERP,D) - augmented gradient +C Input, INTEGER NINTERP - number of points +C Input, INTEGER D - dimension +C Input, INTEGER MAXLEVVEC(D) - max level per dim +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NINTERP, D + DOUBLE PRECISION IPDER(NINTERP, D) + DOUBLE PRECISION IPDER2(NINTERP, D) + INTEGER MAXLEVVEC(D) + DOUBLE PRECISION Y(NINTERP, D) + + INTEGER K, L, MAXLEV, XP + DOUBLE PRECISION YT, YTD1, STEPSIZE, HALFSTEP + DOUBLE PRECISION IPD1, IPD2, ALPHA + + DO L = 1, D + MAXLEV = MAXLEVVEC(L) + IF (MAXLEV .EQ. 0) GOTO 200 + + STEPSIZE = 1.0D+00 / DBLE(2**MAXLEV) + HALFSTEP = STEPSIZE / 2.0D+00 + + DO K = 1, NINTERP + YT = Y(K, L) + IPD1 = IPDER(K, L) + IPD2 = IPDER2(K, L) + +C Find the cell center ytd1 + XP = INT(YT / STEPSIZE) + IF (XP .GE. 2**MAXLEV) XP = 2**MAXLEV - 1 + YTD1 = DBLE(XP)*STEPSIZE + HALFSTEP + + IF (IPD1 * IPD2 .GE. 0.0D+00) THEN +C Same sign: linear blend + IF (STEPSIZE .GT. 0.0D+00) THEN + ALPHA = (YT - YTD1) / STEPSIZE + IF (ALPHA .LT. 0.0D+00) ALPHA = 0.0D+00 + IF (ALPHA .GT. 1.0D+00) ALPHA = 1.0D+00 + IPDER(K, L) = (1.0D+00 - ALPHA)*IPD1 + & + ALPHA*IPD2 + END IF + + ELSE IF (YT .LE. YTD1 + HALFSTEP) THEN +C Left half of cell: taper ipd1 toward zero + IF (HALFSTEP .GT. 0.0D+00) THEN + ALPHA = (YT - YTD1) / HALFSTEP + IF (ALPHA .LT. 0.0D+00) ALPHA = 0.0D+00 + IF (ALPHA .GT. 1.0D+00) ALPHA = 1.0D+00 + IPDER(K, L) = IPD1 * (1.0D+00 - ALPHA) + END IF + + ELSE +C Right half of cell: ramp from zero toward ipd2 + IF (HALFSTEP .GT. 0.0D+00) THEN + ALPHA = (YT - YTD1 - HALFSTEP) / HALFSTEP + IF (ALPHA .LT. 0.0D+00) ALPHA = 0.0D+00 + IF (ALPHA .GT. 1.0D+00) ALPHA = 1.0D+00 + IPDER(K, L) = IPD2 * ALPHA + END IF + END IF + + END DO + + 200 CONTINUE + END DO + + RETURN + END diff --git a/src/reordervals.f b/src/reordervals.f new file mode 100644 index 0000000..453e9b5 --- /dev/null +++ b/src/reordervals.f @@ -0,0 +1,161 @@ +C ******************************************************************* +C +C REORDER_VALS - Reorder hierarchical surplus array for CB grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE REORDER_VALS(Z, NZ, LEVELSEQ, NLEVELS, D) +C ******************************************************************* +C +C REORDER_VALS permutes the hierarchical surplus array Z so that +C dimensions are sorted by level descending within each subgrid. +C This matches the layout expected by BARY_PD_STEP_CB. +C +C Uses a temporary buffer ZTMP of size MAXA=16384. +C Permutation is implemented by building old->new index mappings +C and copying in and out of the temp buffer. +C +C Parameters: +C +C In/out, DOUBLE PRECISION Z(NZ) - surpluses to reorder +C Input, INTEGER NZ - length of Z +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index set +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER MAXA + PARAMETER (MAXA = 16384) + + INTEGER NZ, NLEVELS, D + DOUBLE PRECISION Z(NZ) + INTEGER LEVELSEQ(NLEVELS, D) + + INTEGER KL, K, L, LVAL, NPTS + INTEGER NPTS_DIM(50), ORDER(50), ORDERARR(50), LEVEL(50) + INTEGER NEWPTS_DIM(50) + INTEGER STRIDE_OLD(50), STRIDE_NEW(50) + INTEGER FLAT_OLD, FLAT_NEW + INTEGER INDEX + INTEGER COORD(50), COORD_PERM(50) + INTEGER DIM_PERM + DOUBLE PRECISION ZTMP(MAXA) + INTEGER I, J, TMP + +C Working index into Z + INDEX = 1 + + DO KL = 1, NLEVELS + +C Compute npts per dim and total + NPTS = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + LEVEL(L) = LVAL + IF (LVAL .EQ. 0) THEN + NPTS_DIM(L) = 1 + ELSE IF (LVAL .LE. 2) THEN + NPTS_DIM(L) = 2 + ELSE + NPTS_DIM(L) = 2**(LVAL-1) + END IF + NPTS = NPTS * NPTS_DIM(L) + END DO + + IF (NPTS .EQ. 1) THEN + INDEX = INDEX + 1 + GOTO 100 + END IF + +C Build sort order: insertion sort by level descending + DO L = 1, D + ORDER(L) = L + END DO + DO L = 2, D + TMP = ORDER(L) + J = L - 1 + 10 IF (J .GE. 1 .AND. + & LEVEL(ORDER(J)) .LT. LEVEL(TMP)) THEN + ORDER(J+1) = ORDER(J) + J = J - 1 + GOTO 10 + END IF + ORDER(J+1) = TMP + END DO + +C Check if order is identity - if so, skip + DO L = 1, D + IF (ORDER(L) .NE. L) GOTO 20 + END DO + INDEX = INDEX + NPTS + GOTO 100 + + 20 CONTINUE + +C Build inverse permutation: ORDERARR(dim_new_pos) = dim_old_pos +C ORDER(l) = which old dim goes to new position l +C So new dim l has points from old dim ORDER(l) + DO L = 1, D + NEWPTS_DIM(L) = NPTS_DIM(ORDER(L)) + END DO + +C Compute strides for old layout (Fortran col-major over dims 1..D) + STRIDE_OLD(1) = 1 + DO L = 2, D + STRIDE_OLD(L) = STRIDE_OLD(L-1) * NPTS_DIM(L-1) + END DO + +C Compute strides for new layout (after permutation) + STRIDE_NEW(1) = 1 + DO L = 2, D + STRIDE_NEW(L) = STRIDE_NEW(L-1) * NEWPTS_DIM(L-1) + END DO + +C Copy permuted data into ZTMP +C For each flat new index, compute old flat index + DO I = 0, NPTS - 1 +C Decode new flat index into coordinates + K = I + DO L = D, 1, -1 + COORD(L) = K / STRIDE_NEW(L) + K = K - COORD(L) * STRIDE_NEW(L) + END DO + +C COORD(L) is coordinate in new-dim L = old dim ORDER(L) +C Remap to old coordinates: old dim ORDER(L) gets COORD(L) + DO L = 1, D + COORD_PERM(ORDER(L)) = COORD(L) + END DO + +C Compute old flat index + FLAT_OLD = 0 + DO L = 1, D + FLAT_OLD = FLAT_OLD + + & COORD_PERM(L)*STRIDE_OLD(L) + END DO + + ZTMP(I+1) = Z(INDEX + FLAT_OLD) + END DO + +C Copy back + DO I = 0, NPTS - 1 + Z(INDEX + I) = ZTMP(I+1) + END DO + + INDEX = INDEX + NPTS + + 100 CONTINUE + END DO + + RETURN + END diff --git a/src/sortheap.f b/src/sortheap.f new file mode 100644 index 0000000..35d7d6f --- /dev/null +++ b/src/sortheap.f @@ -0,0 +1,54 @@ +C ******************************************************************* +C +C SORT_HEAP - Max-heap sift-up after inserting at position NA. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SORT_HEAP(A, NA, G, NG) +C ******************************************************************* +C +C SORT_HEAP sifts up the element at position NA in the heap A, +C restoring the max-heap property with respect to priorities G. +C +C The heap stores indices into G; A(k) is an index such that +C G(A(k)) is the priority at heap position k. +C +C Parameters: +C +C In/out, INTEGER A(NA) - heap indices (1-based) +C Input, INTEGER NA - current heap size (element added) +C Input, DOUBLE PRECISION G(NG) - priority values +C Input, INTEGER NG - length of G +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NA, NG + INTEGER A(NA) + DOUBLE PRECISION G(NG) + + INTEGER K, PREV, TMP + + K = NA + + 10 IF (K .GT. 1) THEN + PREV = K / 2 + IF (G(A(PREV)) .LT. G(A(K))) THEN + TMP = A(PREV) + A(PREV) = A(K) + A(K) = TMP + K = PREV + GOTO 10 + END IF + END IF + + RETURN + END diff --git a/src/spcmpvalscb.f b/src/spcmpvalscb.f new file mode 100644 index 0000000..cd5ea55 --- /dev/null +++ b/src/spcmpvalscb.f @@ -0,0 +1,163 @@ +C ******************************************************************* +C +C SPCMPVALS_CB - Compute hierarchical surpluses, Chebyshev grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPCMPVALS_CB(D, Z, NZ, Y, NY, + & NEWLEVELSEQ, NNEWLEVELS, + & LEVELSEQ, NLEVELS, IP) +C ******************************************************************* +C +C SPCMPVALS_CB computes the interpolant at new Chebyshev grid +C points Y using old surpluses Z from previous levels. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - old surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NY,D) - new grid points +C Input, INTEGER NY - rows in Y +C Input, INTEGER NEWLEVELSEQ(NNEWLEVELS,D) - new multi-indices +C Input, INTEGER NNEWLEVELS - rows in NEWLEVELSEQ +C Input, INTEGER LEVELSEQ(NLEVELS,D) - old multi-indices +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NY) - interpolant at Y +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NY, NNEWLEVELS, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NY, D) + INTEGER NEWLEVELSEQ(NNEWLEVELS, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NY) + + INTEGER KL, NKL, K, L, LVAL, NPTS, NDIMS, INDEX + INTEGER ALLNX(50), DIMS(50), LEVEL(50), ORDERARR(50) + INTEGER NNEWPTS(512), SKIPLEVEL, XTOT + INTEGER KSTART, KEND + DOUBLE PRECISION XBUF(16384) + DOUBLE PRECISION IPTEMP(65536) + + DO K = 1, NY + IP(K) = 0.0D+00 + END DO + +C Pre-count new subgrid points + DO NKL = 1, NNEWLEVELS + NPTS = 1 + DO L = 1, D + LVAL = NEWLEVELSEQ(NKL, L) + IF (LVAL .EQ. 0) THEN + ELSE IF (LVAL .LE. 2) THEN + NPTS = NPTS * 2 + ELSE + NPTS = NPTS * 2**(LVAL-1) + END IF + END DO + NNEWPTS(NKL) = NPTS + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + + NPTS = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + LEVEL(L) = LVAL + IF (LVAL .EQ. 0) THEN + ELSE IF (LVAL .LE. 2) THEN + NPTS = NPTS * 2 + ELSE + NPTS = NPTS * 2**(LVAL-1) + END IF + END DO + + IF (NPTS .GT. 1) THEN +C Sort dimensions descending by level + DO L = 1, D + ORDERARR(L) = L + END DO + DO L = 1, D-1 + DO K = L+1, D + IF (LEVEL(ORDERARR(K)) .GT. + & LEVEL(ORDERARR(L))) THEN + LVAL = ORDERARR(L) + ORDERARR(L) = ORDERARR(K) + ORDERARR(K) = LVAL + END IF + END DO + END DO + + NDIMS = 0 + XTOT = 0 + DO L = 1, D + LVAL = LEVEL(ORDERARR(L)) + IF (LVAL .GT. 0) THEN + NDIMS = NDIMS + 1 + DIMS(NDIMS) = ORDERARR(L) + ALLNX(NDIMS) = 2**LVAL + 1 + XTOT = XTOT + ALLNX(NDIMS) + END IF + END DO + + CALL GET_CHEB_NODES(ALLNX, NDIMS, XBUF, XTOT) + + KSTART = 1 + DO NKL = 1, NNEWLEVELS + KEND = KSTART + NNEWPTS(NKL) - 1 + + SKIPLEVEL = 0 + DO L = 1, D + IF (LEVELSEQ(KL,L) .GT. + & NEWLEVELSEQ(NKL,L)) THEN + SKIPLEVEL = 1 + GOTO 80 + END IF + END DO + + 80 IF (SKIPLEVEL .EQ. 0) THEN + DO K = 1, KEND - KSTART + 1 + IPTEMP(K) = 0.0D+00 + END DO + CALL BARY_PD_STEP_CB( + & Z(INDEX), NPTS, + & ALLNX, DIMS, NDIMS, + & XBUF, XTOT, + & Y(KSTART,1), + & KEND-KSTART+1, D, + & IPTEMP) + DO K = KSTART, KEND + IP(K) = IP(K) + + & IPTEMP(K-KSTART+1) + END DO + END IF + + KSTART = KEND + 1 + END DO + + ELSE +C Single-node subgrid: constant contribution + DO K = 1, NY + IP(K) = IP(K) + Z(INDEX) + END DO + END IF + + INDEX = INDEX + NPTS + + END DO + + RETURN + END diff --git a/src/spcmpvalscbdct.f b/src/spcmpvalscbdct.f new file mode 100644 index 0000000..1478cc0 --- /dev/null +++ b/src/spcmpvalscbdct.f @@ -0,0 +1,147 @@ +C ******************************************************************* +C +C SPCMPVALS_CB_DCT - CB surplus computation using DCT. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPCMPVALS_CB_DCT(D, Z, NZ, Y, NY, + & NEWLEVELSEQ, NNEWLEVELS, + & LEVELSEQ, NLEVELS, IP) +C ******************************************************************* +C +C SPCMPVALS_CB_DCT computes the hierarchical surplus increments for +C Chebyshev grid using DCT-based upsampling instead of barycentric +C interpolation. +C +C For each old subgrid (LEVELSEQ row kl), for each new subgrid +C (NEWLEVELSEQ row nkl) that it contributes to, calls SP_DCT_UP_STEP. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - old surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NY,D) - new grid points +C Input, INTEGER NY - rows in Y +C Input, INTEGER NEWLEVELSEQ(NNEWLEVELS,D) - new multi-indices +C Input, INTEGER NNEWLEVELS - rows in NEWLEVELSEQ +C Input, INTEGER LEVELSEQ(NLEVELS,D) - old multi-indices +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NY) - interpolant at Y +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NY, NNEWLEVELS, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NY, D) + INTEGER NEWLEVELSEQ(NNEWLEVELS, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NY) + + INTEGER KL, NKL, K, L, LVAL, NPTS, NDIMS + INTEGER INDEX, KSTART, KEND + INTEGER NNEWPTS(512), SKIPLEVEL + INTEGER OLDLEV(50), NEWLEV(50), DIMS(50) + DOUBLE PRECISION IPBUF(16384) + + DO K = 1, NY + IP(K) = 0.0D+00 + END DO + +C Pre-count new subgrid points + DO NKL = 1, NNEWLEVELS + NPTS = 1 + DO L = 1, D + LVAL = NEWLEVELSEQ(NKL, L) + IF (LVAL .EQ. 0) THEN + ELSE IF (LVAL .LE. 2) THEN + NPTS = NPTS * 2 + ELSE + NPTS = NPTS * 2**(LVAL-1) + END IF + END DO + NNEWPTS(NKL) = NPTS + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + +C Count old subgrid points and collect active dims/levels + NPTS = 1 + NDIMS = 0 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .GT. 0) THEN + NDIMS = NDIMS + 1 + OLDLEV(NDIMS) = LVAL + DIMS(NDIMS) = L + IF (LVAL .LE. 2) THEN + NPTS = NPTS * 2 + ELSE + NPTS = NPTS * 2**(LVAL-1) + END IF + END IF + END DO + + IF (NPTS .GT. 1) THEN + + KSTART = 1 + DO NKL = 1, NNEWLEVELS + KEND = KSTART + NNEWPTS(NKL) - 1 + + SKIPLEVEL = 0 + DO L = 1, D + IF (LEVELSEQ(KL,L) .GT. + & NEWLEVELSEQ(NKL,L)) THEN + SKIPLEVEL = 1 + GOTO 80 + END IF + END DO + + 80 IF (SKIPLEVEL .EQ. 0) THEN +C Collect new levels for this NKL + DO L = 1, NDIMS + NEWLEV(L) = NEWLEVELSEQ(NKL,DIMS(L)) + END DO + + DO K = 1, KEND - KSTART + 1 + IPBUF(K) = 0.0D+00 + END DO + + CALL SP_DCT_UP_STEP(Z(INDEX), NPTS, + & OLDLEV, NDIMS, + & NEWLEV, NDIMS, DIMS, + & IPBUF, KEND-KSTART+1) + + DO K = KSTART, KEND + IP(K) = IP(K) + + & IPBUF(K-KSTART+1) + END DO + END IF + + KSTART = KEND + 1 + END DO + + ELSE +C Single-node constant subgrid + DO K = 1, NY + IP(K) = IP(K) + Z(INDEX) + END DO + END IF + + INDEX = INDEX + NPTS + + END DO + + RETURN + END diff --git a/src/spcmpvalscbgpsp.f b/src/spcmpvalscbgpsp.f new file mode 100644 index 0000000..7d123dc --- /dev/null +++ b/src/spcmpvalscbgpsp.f @@ -0,0 +1,207 @@ +C ******************************************************************* +C +C SPCMPVALS_CBGP_SP - CB/GP hierarchical surpluses, sparse struct. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPCMPVALS_CBGP_SP(D, Z, NZ, Y, NY, + & INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, NADDR, + & BACKWARDNEIGHBORS, FORWARDNEIGHBORS, NFWD, + & SUBGRIDPOINTS, SUBGRIDADDR, + & FROMINDEX, TOINDEX, ISGP, IP) +C ******************************************************************* +C +C SPCMPVALS_CBGP_SP computes hierarchical surplus increments for +C new subgrids using Chebyshev (ISGP=0) or GP (ISGP=1) barycentric +C interpolation and the sparse index structure. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - old surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NY,D) - new grid points +C Input, INTEGER NY - rows in Y +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER BACKWARDNEIGHBORS(NADDR) - back neighbor per dim +C Input, INTEGER FORWARDNEIGHBORS(NFWD) - fwd neighbor flat array +C Input, INTEGER NFWD - length of fwd array +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Input, INTEGER SUBGRIDADDR(NSUBGRIDS) - start Z addr per subgrid +C Input, INTEGER FROMINDEX - first new subgrid +C Input, INTEGER TOINDEX - last new subgrid +C Input, INTEGER ISGP - 0=Cheb, 1=GP +C Output, DOUBLE PRECISION IP(NY) - interpolant at Y +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NY, NSUBGRIDS, NADDR, NFWD + INTEGER FROMINDEX, TOINDEX, ISGP + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NY, D) + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER BACKWARDNEIGHBORS(NADDR) + INTEGER FORWARDNEIGHBORS(NFWD) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + INTEGER SUBGRIDADDR(NSUBGRIDS) + DOUBLE PRECISION IP(NY) + + INTEGER NKL, CI, DID, K, L + INTEGER NDIMS, ADDR + INTEGER NEWLEV(50), DIMVEC(50) + INTEGER LVAL, NPTS + INTEGER ANCACTIVEDIMS(50), ANCACTIVELEV(50) + INTEGER ALLNX(50), DIMS(50), XTOT + INTEGER SKIPLEVEL + INTEGER ANCNDIMS, ANCADDR, ANCNPTS + INTEGER KSTART, KEND, INDEX + INTEGER ORDERARR(50), TMP, I, J + DOUBLE PRECISION XBUF(16384), WBUF(16384) + DOUBLE PRECISION IPTEMP(65536) + + DO K = 1, NY + IP(K) = 0.0D+00 + END DO + + KSTART = 1 + DO CI = 1, FROMINDEX - 1 + KSTART = KSTART + SUBGRIDPOINTS(CI) + END DO + + DO NKL = FROMINDEX, TOINDEX + + NDIMS = INDICESNDIIMS(NKL) + ADDR = INDICESADDR(NKL) + NPTS = SUBGRIDPOINTS(NKL) + KEND = KSTART + NPTS - 1 + + DO DID = 1, NDIMS + DIMVEC(DID) = INDICESDIMS(ADDR + DID - 1) + NEWLEV(DID) = INDICESLEVS(ADDR + DID - 1) + END DO + +C Walk all ancestor subgrids + DO CI = 1, NSUBGRIDS + IF (CI .GE. FROMINDEX) GOTO 300 + + ANCNDIMS = INDICESNDIIMS(CI) + ANCADDR = INDICESADDR(CI) + ANCNPTS = SUBGRIDPOINTS(CI) + INDEX = SUBGRIDADDR(CI) + + SKIPLEVEL = 0 + DO L = 1, ANCNDIMS + ANCACTIVEDIMS(L) = INDICESDIMS(ANCADDR + L - 1) + ANCACTIVELEV(L) = INDICESLEVS(ANCADDR + L - 1) + END DO + + DO L = 1, ANCNDIMS + DID = 0 + DO K = 1, NDIMS + IF (ANCACTIVEDIMS(L) .EQ. DIMVEC(K)) THEN + IF (ANCACTIVELEV(L) .LE. + & NEWLEV(K)) THEN + DID = K + ELSE + SKIPLEVEL = 1 + END IF + END IF + END DO + IF (DID .EQ. 0 .AND. + & ANCACTIVELEV(L) .GT. 0) THEN + SKIPLEVEL = 1 + END IF + IF (SKIPLEVEL .EQ. 1) GOTO 300 + END DO + + IF (ANCNPTS .EQ. 1) THEN +C Constant subgrid: add Z(INDEX) to all new points + DO K = KSTART, KEND + IP(K) = IP(K) + Z(INDEX) + END DO + GOTO 300 + END IF + +C Build ALLNX/DIMS sorted by level descending + DO L = 1, ANCNDIMS + ORDERARR(L) = L + END DO + DO I = 2, ANCNDIMS + TMP = ORDERARR(I) + J = I - 1 + 10 IF (J .GE. 1 .AND. + & ANCACTIVELEV(ORDERARR(J)) .LT. + & ANCACTIVELEV(TMP)) THEN + ORDERARR(J+1) = ORDERARR(J) + J = J - 1 + GOTO 10 + END IF + ORDERARR(J+1) = TMP + END DO + + XTOT = 0 + DO I = 1, ANCNDIMS + L = ORDERARR(I) + LVAL = ANCACTIVELEV(L) + IF (ISGP .EQ. 0) THEN + ALLNX(I) = 2**LVAL + 1 + ELSE + ALLNX(I) = 2**(LVAL+1) - 1 + END IF + DIMS(I) = ANCACTIVEDIMS(L) + XTOT = XTOT + ALLNX(I) + END DO + + DO K = 1, KEND - KSTART + 1 + IPTEMP(K) = 0.0D+00 + END DO + + IF (ISGP .EQ. 0) THEN + CALL GET_CHEB_NODES(ALLNX, ANCNDIMS, + & XBUF, XTOT) + CALL BARY_PD_STEP_CB(Z(INDEX), ANCNPTS, + & ALLNX, DIMS, ANCNDIMS, + & XBUF, XTOT, + & Y(KSTART,1), KEND-KSTART+1, D, + & IPTEMP) + ELSE + CALL GET_GP_NODES(ALLNX, ANCNDIMS, XBUF, XTOT) + CALL GET_GP_BARY_W(ALLNX, ANCNDIMS, WBUF, XTOT) + CALL BARY_PD_STEP_GP(Z(INDEX), ANCNPTS, + & ALLNX, DIMS, ANCNDIMS, + & XBUF, XTOT, + & Y(KSTART,1), KEND-KSTART+1, D, + & WBUF, IPTEMP) + END IF + + DO K = KSTART, KEND + IP(K) = IP(K) + IPTEMP(K - KSTART + 1) + END DO + + 300 CONTINUE + END DO + + KSTART = KEND + 1 + + END DO + + RETURN + END diff --git a/src/spcmpvalscbspdct.f b/src/spcmpvalscbspdct.f new file mode 100644 index 0000000..7ce0918 --- /dev/null +++ b/src/spcmpvalscbspdct.f @@ -0,0 +1,169 @@ +C ******************************************************************* +C +C SPCMPVALS_CB_SP_DCT - DCT surplus computation, sparse indices. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPCMPVALS_CB_SP_DCT(D, Z, NZ, Y, NY, + & INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, NADDR, + & BACKWARDNEIGHBORS, FORWARDNEIGHBORS, NFWD, + & SUBGRIDPOINTS, SUBGRIDADDR, + & FROMINDEX, TOINDEX, IP) +C ******************************************************************* +C +C SPCMPVALS_CB_SP_DCT computes hierarchical surplus increments for +C Chebyshev grid using DCT-based upsampling and sparse index +C structure with ancestor traversal. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - old surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NY,D) - new grid points +C Input, INTEGER NY - rows in Y +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER BACKWARDNEIGHBORS(NADDR) - back neighbor per dim +C Input, INTEGER FORWARDNEIGHBORS(NFWD) - fwd neighbor flat array +C Input, INTEGER NFWD - length of fwd array +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Input, INTEGER SUBGRIDADDR(NSUBGRIDS) - start Z addr per subgrid +C Input, INTEGER FROMINDEX - first new subgrid +C Input, INTEGER TOINDEX - last new subgrid +C Output, DOUBLE PRECISION IP(NY) - interpolant at Y +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NY, NSUBGRIDS, NADDR, NFWD + INTEGER FROMINDEX, TOINDEX + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NY, D) + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER BACKWARDNEIGHBORS(NADDR) + INTEGER FORWARDNEIGHBORS(NFWD) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + INTEGER SUBGRIDADDR(NSUBGRIDS) + DOUBLE PRECISION IP(NY) + + INTEGER NKL, CI, DID, K, L + INTEGER NDIMS, ADDR + INTEGER NEWLEV(50), DIMVEC(50) + INTEGER ANCACTIVEDIMS(50), ANCACTIVELEV(50) + INTEGER ANCNEWLEV(50), ANCDIMS(50) + INTEGER SKIPLEVEL, ANCNDIMS, ANCADDR, ANCNPTS + INTEGER KSTART, KEND, INDEX, NPTS + DOUBLE PRECISION IPBUF(16384) + + DO K = 1, NY + IP(K) = 0.0D+00 + END DO + + KSTART = 1 + DO CI = 1, FROMINDEX - 1 + KSTART = KSTART + SUBGRIDPOINTS(CI) + END DO + + DO NKL = FROMINDEX, TOINDEX + + NDIMS = INDICESNDIIMS(NKL) + ADDR = INDICESADDR(NKL) + NPTS = SUBGRIDPOINTS(NKL) + KEND = KSTART + NPTS - 1 + + DO DID = 1, NDIMS + DIMVEC(DID) = INDICESDIMS(ADDR + DID - 1) + NEWLEV(DID) = INDICESLEVS(ADDR + DID - 1) + END DO + +C Walk all ancestor subgrids + DO CI = 1, NSUBGRIDS + IF (CI .GE. FROMINDEX) GOTO 300 + + ANCNDIMS = INDICESNDIIMS(CI) + ANCADDR = INDICESADDR(CI) + ANCNPTS = SUBGRIDPOINTS(CI) + INDEX = SUBGRIDADDR(CI) + + SKIPLEVEL = 0 + DO L = 1, ANCNDIMS + ANCACTIVEDIMS(L) = INDICESDIMS(ANCADDR + L - 1) + ANCACTIVELEV(L) = INDICESLEVS(ANCADDR + L - 1) + END DO + + DO L = 1, ANCNDIMS + DID = 0 + DO K = 1, NDIMS + IF (ANCACTIVEDIMS(L) .EQ. DIMVEC(K)) THEN + IF (ANCACTIVELEV(L) .LE. + & NEWLEV(K)) THEN + DID = K + ELSE + SKIPLEVEL = 1 + END IF + END IF + END DO + IF (DID .EQ. 0 .AND. + & ANCACTIVELEV(L) .GT. 0) THEN + SKIPLEVEL = 1 + END IF + IF (SKIPLEVEL .EQ. 1) GOTO 300 + END DO + + IF (ANCNPTS .EQ. 1) THEN + DO K = KSTART, KEND + IP(K) = IP(K) + Z(INDEX) + END DO + GOTO 300 + END IF + +C Collect new levels for ancestor dims + DO L = 1, ANCNDIMS + ANCDIMS(L) = ANCACTIVEDIMS(L) + ANCNEWLEV(L) = 0 + DO K = 1, NDIMS + IF (DIMVEC(K) .EQ. ANCDIMS(L)) THEN + ANCNEWLEV(L) = NEWLEV(K) + END IF + END DO + END DO + + DO K = 1, KEND - KSTART + 1 + IPBUF(K) = 0.0D+00 + END DO + + CALL SP_DCT_UP_STEP(Z(INDEX), ANCNPTS, + & ANCACTIVELEV, ANCNDIMS, + & ANCNEWLEV, ANCNDIMS, ANCDIMS, + & IPBUF, KEND-KSTART+1) + + DO K = KSTART, KEND + IP(K) = IP(K) + IPBUF(K - KSTART + 1) + END DO + + 300 CONTINUE + END DO + + KSTART = KEND + 1 + + END DO + + RETURN + END diff --git a/src/spcmpvalscc.f b/src/spcmpvalscc.f new file mode 100644 index 0000000..0af5414 --- /dev/null +++ b/src/spcmpvalscc.f @@ -0,0 +1,182 @@ +C ******************************************************************* +C +C SPCMPVALS_CC - Compute hierarchical surpluses, +C Clenshaw-Curtis sparse grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPCMPVALS_CC(D, Z, NZ, Y, NY, + & NEWLEVELSEQ, NNEWLEVELS, + & LEVELSEQ, NLEVELS, IP) +C ******************************************************************* +C +C SPCMPVALS_CC computes the hierarchical surplus increments for +C the NEW subgrid levels (NEWLEVELSEQ) using the already-computed +C surpluses from previous levels (Z, LEVELSEQ). +C +C Caller subtracts IP from the actual function values at Y to +C obtain the true surpluses for the new subgrid. +C +C Algorithm mirrors MATLAB spcmpvalscc: +C For each old subgrid (LEVELSEQ row kl): +C For each new subgrid (NEWLEVELSEQ row nkl): +C Skip if any level(l) > newlevel(l) (no contribution). +C Otherwise accumulate hat-function weights times surplus. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - old surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NY,D) - new grid points +C Input, INTEGER NY - rows in Y +C Input, INTEGER NEWLEVELSEQ(NNEWLEVELS,D) - new multi-indices +C Input, INTEGER NNEWLEVELS - rows in NEWLEVELSEQ +C Input, INTEGER LEVELSEQ(NLEVELS,D) - old multi-indices +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NY) - interpolant at Y +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NY, NNEWLEVELS, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NY, D) + INTEGER NEWLEVELSEQ(NNEWLEVELS, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NY) + + INTEGER KL, NKL, K, L + INTEGER LVAL, XP + INTEGER INDEX, INDEX2(D), INDEX3 + INTEGER REPVEC(D), NPTS, SKIPLEVEL + INTEGER NNEWPTS(NNEWLEVELS) + DOUBLE PRECISION TEMP, SCALE, YT + INTEGER KSTART, KEND + + DO K = 1, NY + IP(K) = 0.0D+00 + END DO + +C Pre-compute point counts for each new subgrid. + DO NKL = 1, NNEWLEVELS + NPTS = 1 + DO L = 1, D + LVAL = NEWLEVELSEQ(NKL, L) + IF (LVAL .EQ. 0) THEN + CONTINUE + ELSE IF (LVAL .LT. 3) THEN + NPTS = NPTS * 2 + ELSE + NPTS = NPTS * 2**(LVAL-1) + END IF + END DO + NNEWPTS(NKL) = NPTS + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + +C Build repvec (cumulative strides) for old subgrid kl. + NPTS = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .EQ. 0) THEN + REPVEC(L) = 1 + ELSE IF (LVAL .LT. 3) THEN + REPVEC(L) = 2 + ELSE + REPVEC(L) = 2**(LVAL-1) + END IF + NPTS = NPTS * REPVEC(L) + IF (L .GT. 1) THEN + REPVEC(L) = REPVEC(L) * REPVEC(L-1) + END IF + END DO + +C Walk through new subgrids to determine which Y rows this +C old subgrid contributes to. + KSTART = 1 + DO NKL = 1, NNEWLEVELS + KEND = KSTART + NNEWPTS(NKL) - 1 + +C Skip if old level exceeds new level in any dim. + SKIPLEVEL = 0 + DO L = 1, D + IF (LEVELSEQ(KL,L) .GT. + & NEWLEVELSEQ(NKL,L)) THEN + SKIPLEVEL = 1 + GOTO 80 + END IF + END DO + + 80 IF (SKIPLEVEL .EQ. 0) THEN + DO K = KSTART, KEND + TEMP = 1.0D+00 + L = 1 + + 10 IF (L .LE. D) THEN + LVAL = LEVELSEQ(KL, L) + YT = Y(K, L) + + IF (LVAL .EQ. 0) THEN + INDEX2(L) = 0 + + ELSE IF (LVAL .EQ. 1) THEN + IF (YT .EQ. 1.0D+00) THEN + INDEX2(L) = 1 + ELSE + XP = INT(YT*2.0D+00) + IF (XP .EQ. 0) THEN + TEMP = TEMP * + & 2.0D+00 * + & (0.5D+00 - YT) + ELSE + TEMP = TEMP * + & 2.0D+00 * + & (YT - 0.5D+00) + END IF + INDEX2(L) = XP + END IF + + ELSE + SCALE = DBLE(2**LVAL) + XP = INT(YT*SCALE/2.0D+00) + TEMP = TEMP * (1.0D+00 + & - SCALE * DABS(YT + & - DBLE(XP*2+1)/SCALE)) + INDEX2(L) = XP + END IF + + L = L + 1 + GOTO 10 + END IF + + INDEX3 = INDEX + INDEX2(1) + DO L = 2, D + INDEX3 = INDEX3 + + & REPVEC(L-1) * INDEX2(L) + END DO + IP(K) = IP(K) + TEMP * Z(INDEX3) + + END DO + END IF + + KSTART = KEND + 1 + END DO + + INDEX = INDEX + NPTS + + END DO + + RETURN + END diff --git a/src/spcmpvalsccsp.f b/src/spcmpvalsccsp.f new file mode 100644 index 0000000..6dcd025 --- /dev/null +++ b/src/spcmpvalsccsp.f @@ -0,0 +1,237 @@ +C ******************************************************************* +C +C SPCMPVALS_CC_SP - CC hierarchical surpluses, sparse index struct. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPCMPVALS_CC_SP(D, Z, NZ, Y, NY, + & INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, NADDR, + & BACKWARDNEIGHBORS, FORWARDNEIGHBORS, NFWD, + & SUBGRIDPOINTS, SUBGRIDADDR, + & FROMINDEX, TOINDEX, IP) +C ******************************************************************* +C +C SPCMPVALS_CC_SP computes the hierarchical surplus increments for +C new subgrids FROMINDEX..TOINDEX using a sparse index structure +C and neighbor traversal (ancestor walk). +C +C For each new subgrid, walks back through ancestor subgrids using +C BACKWARDNEIGHBORS / FORWARDNEIGHBORS, applies hat function +C accumulation from each ancestor. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - old surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NY,D) - new grid points +C Input, INTEGER NY - rows in Y +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER BACKWARDNEIGHBORS(NADDR) - back neighbor per dim +C Input, INTEGER FORWARDNEIGHBORS(NFWD) - fwd neighbor flat array +C Input, INTEGER NFWD - length of fwd array +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Input, INTEGER SUBGRIDADDR(NSUBGRIDS) - start Z addr per subgrid +C Input, INTEGER FROMINDEX - first new subgrid +C Input, INTEGER TOINDEX - last new subgrid +C Output, DOUBLE PRECISION IP(NY) - interpolant at Y +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NY, NSUBGRIDS, NADDR, NFWD + INTEGER FROMINDEX, TOINDEX + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NY, D) + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER BACKWARDNEIGHBORS(NADDR) + INTEGER FORWARDNEIGHBORS(NFWD) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + INTEGER SUBGRIDADDR(NSUBGRIDS) + DOUBLE PRECISION IP(NY) + +C Local variables + INTEGER NKL, CI, DID, K, L + INTEGER NDIMS, ADDR + INTEGER NEWLEV(50), DIMVEC(50) + INTEGER CURLEV(50), CURINDEX + INTEGER LVAL, NPTS + INTEGER ACTIVEDIMS(50), ACTIVELEV(50) + INTEGER REPVEC(50), NPTS_DIM(50) + INTEGER INDEX2(50), INDEX3, INDEX + INTEGER XP, KSTART, KEND + DOUBLE PRECISION YT, TEMP, SCALE + INTEGER STACKIDX(100), STACKDID(100), NSTACK + INTEGER MAXANCESTORS, IANC + INTEGER ANCINDEX, ANCADDR, ANCNDIMS, ANCNPTS + INTEGER ANCACTIVEDIMS(50), ANCACTIVELEV(50) + INTEGER SKIPLEVEL + + DO K = 1, NY + IP(K) = 0.0D+00 + END DO + +C Compute kstart/kend offsets for new subgrids + KSTART = 1 + DO CI = 1, FROMINDEX - 1 + KSTART = KSTART + SUBGRIDPOINTS(CI) + END DO + + DO NKL = FROMINDEX, TOINDEX + + NDIMS = INDICESNDIIMS(NKL) + ADDR = INDICESADDR(NKL) + NPTS = SUBGRIDPOINTS(NKL) + KEND = KSTART + NPTS - 1 + +C Read new subgrid's active dims and levels + DO DID = 1, NDIMS + DIMVEC(DID) = INDICESDIMS(ADDR + DID - 1) + NEWLEV(DID) = INDICESLEVS(ADDR + DID - 1) + CURLEV(DID) = 0 + END DO + +C Walk all ancestors using nested loop descent +C Ancestor subgrid starts at the (0,0,...,0) level subgrid +C and climbs up through all levels up to NEWLEV. +C Use a simple exhaustive approach: iterate over all old subgrids +C and check if they are ancestors of NKL. + + DO CI = 1, NSUBGRIDS +C Skip new subgrids themselves + IF (CI .GE. FROMINDEX) GOTO 300 + + ANCNDIMS = INDICESNDIIMS(CI) + ANCADDR = INDICESADDR(CI) + ANCNPTS = SUBGRIDPOINTS(CI) + +C Check if CI is an ancestor: all its active dims must have +C levels <= NEWLEV in the same dim, and only dims in DIMVEC + SKIPLEVEL = 0 + DO L = 1, ANCNDIMS + ANCACTIVEDIMS(L) = INDICESDIMS(ANCADDR + L - 1) + ANCACTIVELEV(L) = INDICESLEVS(ANCADDR + L - 1) + END DO + +C Each ancestor dim must match a dim in DIMVEC and have lev<= + DO L = 1, ANCNDIMS + DID = 0 + DO K = 1, NDIMS + IF (ANCACTIVEDIMS(L) .EQ. DIMVEC(K)) THEN + IF (ANCACTIVELEV(L) .LE. + & NEWLEV(K)) THEN + DID = K + ELSE + SKIPLEVEL = 1 + END IF + END IF + END DO + IF (DID .EQ. 0 .AND. + & ANCACTIVELEV(L) .GT. 0) THEN + SKIPLEVEL = 1 + END IF + IF (SKIPLEVEL .EQ. 1) GOTO 300 + END DO + +C CI is an ancestor. Apply hat function from CI to Y(KSTART..KEND) + INDEX = SUBGRIDADDR(CI) + +C Build repvec for ancestor + DO L = 1, ANCNDIMS + LVAL = ANCACTIVELEV(L) + IF (LVAL .EQ. 0) THEN + NPTS_DIM(L) = 1 + ELSE IF (LVAL .LT. 3) THEN + NPTS_DIM(L) = 2 + ELSE + NPTS_DIM(L) = 2**(LVAL-1) + END IF + END DO + REPVEC(1) = 1 + DO L = 2, ANCNDIMS + REPVEC(L) = REPVEC(L-1) * NPTS_DIM(L-1) + END DO + + DO K = KSTART, KEND + TEMP = 1.0D+00 + L = 1 + + 10 IF (L .LE. ANCNDIMS) THEN + LVAL = ANCACTIVELEV(L) + YT = Y(K, ANCACTIVEDIMS(L)) + + IF (YT .LT. 0.0D+00) YT = 0.0D+00 + IF (YT .GT. 1.0D+00) YT = 1.0D+00 + + IF (LVAL .EQ. 0) THEN + INDEX2(L) = 0 + + ELSE IF (LVAL .EQ. 1) THEN + IF (YT .EQ. 1.0D+00) THEN + INDEX2(L) = 1 + ELSE + XP = INT(YT * 2.0D+00) + IF (XP .EQ. 0) THEN + TEMP = TEMP * + & 2.0D+00*(0.5D+00-YT) + ELSE + TEMP = TEMP * + & 2.0D+00*(YT-0.5D+00) + END IF + INDEX2(L) = XP + END IF + + ELSE + IF (YT .EQ. 1.0D+00) THEN + TEMP = 0.0D+00 + ELSE + SCALE = DBLE(2**LVAL) + XP = INT(YT*SCALE/2.0D+00) + TEMP = TEMP * (1.0D+00 + & - SCALE * DABS(YT + & - DBLE(XP*2+1)/SCALE)) + INDEX2(L) = XP + END IF + END IF + + IF (TEMP .EQ. 0.0D+00) GOTO 100 + + L = L + 1 + GOTO 10 + END IF + + INDEX3 = INDEX + INDEX2(1) + DO L = 2, ANCNDIMS + INDEX3 = INDEX3 + REPVEC(L)*INDEX2(L) + END DO + IP(K) = IP(K) + TEMP * Z(INDEX3) + + 100 CONTINUE + END DO + + 300 CONTINUE + END DO + + KSTART = KEND + 1 + + END DO + + RETURN + END diff --git a/src/spcmpvalsgp.f b/src/spcmpvalsgp.f new file mode 100644 index 0000000..4ef9cfa --- /dev/null +++ b/src/spcmpvalsgp.f @@ -0,0 +1,134 @@ +C ******************************************************************* +C +C SPCMPVALS_GP - Compute hierarchical surpluses, Gauss-Patterson. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPCMPVALS_GP(D, Z, NZ, Y, NY, + & NEWLEVELSEQ, NNEWLEVELS, + & LEVELSEQ, NLEVELS, IP) +C ******************************************************************* +C +C SPCMPVALS_GP computes interpolant at new GP grid points Y using +C old surpluses Z from previous levels, for surplus subtraction. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - old surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NY,D) - new grid points +C Input, INTEGER NY - rows in Y +C Input, INTEGER NEWLEVELSEQ(NNEWLEVELS,D) - new multi-indices +C Input, INTEGER NNEWLEVELS - rows in NEWLEVELSEQ +C Input, INTEGER LEVELSEQ(NLEVELS,D) - old multi-indices +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NY) - interpolant at Y +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NY, NNEWLEVELS, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NY, D) + INTEGER NEWLEVELSEQ(NNEWLEVELS, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NY) + + INTEGER KL, NKL, K, L, LVAL, NPTS, NDIMS, INDEX + INTEGER ALLNX(50), DIMS(50) + INTEGER NNEWPTS(512), SKIPLEVEL, XTOT + INTEGER KSTART, KEND + DOUBLE PRECISION XBUF(16384), WBUF(16384) + DOUBLE PRECISION IPTEMP(65536) + + DO K = 1, NY + IP(K) = 0.0D+00 + END DO + + DO NKL = 1, NNEWLEVELS + NPTS = 1 + DO L = 1, D + LVAL = NEWLEVELSEQ(NKL, L) + IF (LVAL .GT. 0) NPTS = NPTS * 2**LVAL + END DO + NNEWPTS(NKL) = NPTS + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + + NPTS = 1 + NDIMS = 0 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .GT. 0) THEN + NPTS = NPTS * 2**LVAL + NDIMS = NDIMS + 1 + ALLNX(NDIMS) = 2**(LVAL+1) - 1 + DIMS(NDIMS) = L + END IF + END DO + + IF (NPTS .GT. 1) THEN + XTOT = 0 + DO L = 1, NDIMS + XTOT = XTOT + ALLNX(L) + END DO + CALL GET_GP_NODES(ALLNX, NDIMS, XBUF, XTOT) + CALL GET_GP_BARY_W(ALLNX, NDIMS, WBUF, XTOT) + + KSTART = 1 + DO NKL = 1, NNEWLEVELS + KEND = KSTART + NNEWPTS(NKL) - 1 + + SKIPLEVEL = 0 + DO L = 1, D + IF (LEVELSEQ(KL,L) .GT. + & NEWLEVELSEQ(NKL,L)) THEN + SKIPLEVEL = 1 + GOTO 80 + END IF + END DO + + 80 IF (SKIPLEVEL .EQ. 0) THEN + DO K = 1, KEND-KSTART+1 + IPTEMP(K) = 0.0D+00 + END DO + CALL BARY_PD_STEP_GP( + & Z(INDEX), NPTS, + & ALLNX, DIMS, NDIMS, + & XBUF, XTOT, + & Y(KSTART,1), + & KEND-KSTART+1, D, + & WBUF, IPTEMP) + DO K = KSTART, KEND + IP(K) = IP(K) + + & IPTEMP(K-KSTART+1) + END DO + END IF + + KSTART = KEND + 1 + END DO + + ELSE + DO K = 1, NY + IP(K) = IP(K) + Z(INDEX) + END DO + END IF + + INDEX = INDEX + NPTS + + END DO + + RETURN + END diff --git a/src/spcmpvalsm.f b/src/spcmpvalsm.f new file mode 100644 index 0000000..4b193ce --- /dev/null +++ b/src/spcmpvalsm.f @@ -0,0 +1,215 @@ +C ******************************************************************* +C +C SPCMPVALS_M - Compute hierarchical surpluses, Maximum-norm grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPCMPVALS_M(D, Z, NZ, Y, NY, + & NEWLEVELSEQ, NNEWLEVELS, + & LEVELSEQ, NLEVELS, IP) +C ******************************************************************* +C +C SPCMPVALS_M computes interpolant at new Maximum-norm grid points Y +C using surpluses Z from previous levels, for surplus subtraction. +C +C For lev=0 dimensions the repeat mechanism iterates over all +C 2^nlevelzero combinations of midpoint vs boundary nodes. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - old surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NY,D) - new grid points +C Input, INTEGER NY - rows in Y +C Input, INTEGER NEWLEVELSEQ(NNEWLEVELS,D) - new multi-indices +C Input, INTEGER NNEWLEVELS - rows in NEWLEVELSEQ +C Input, INTEGER LEVELSEQ(NLEVELS,D) - old multi-indices +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NY) - interpolant at Y +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NY, NNEWLEVELS, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NY, D) + INTEGER NEWLEVELSEQ(NNEWLEVELS, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NY) + + INTEGER KL, NKL, K, L + INTEGER LVAL, XP + INTEGER INDEX, INDEX2(50), INDEX3 + INTEGER REPVEC(50), REPEAT(50), NPTS, NLEVELZERO, NREPEATS + INTEGER SKIPLEVEL, NNEWPTS(512) + INTEGER REPSTEP + DOUBLE PRECISION TEMP, SCALE, YT + INTEGER KSTART, KEND + + DO K = 1, NY + IP(K) = 0.0D+00 + END DO + + DO NKL = 1, NNEWLEVELS + NPTS = 1 + DO L = 1, D + LVAL = NEWLEVELSEQ(NKL, L) + IF (LVAL .EQ. 0) THEN + NPTS = NPTS * 3 + ELSE + NPTS = NPTS * 2**LVAL + END IF + END DO + NNEWPTS(NKL) = NPTS + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + + NPTS = 1 + NLEVELZERO = 0 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .EQ. 0) THEN + REPVEC(L) = 3 + NLEVELZERO = NLEVELZERO + 1 + ELSE + REPVEC(L) = 2**LVAL + END IF + REPEAT(L) = 0 + NPTS = NPTS * REPVEC(L) + IF (L .GT. 1) REPVEC(L) = REPVEC(L) * REPVEC(L-1) + END DO + + NREPEATS = 2**NLEVELZERO - 1 + + KSTART = 1 + DO NKL = 1, NNEWLEVELS + KEND = KSTART + NNEWPTS(NKL) - 1 + + SKIPLEVEL = 0 + DO L = 1, D + IF (LEVELSEQ(KL,L) .GT. + & NEWLEVELSEQ(NKL,L)) THEN + SKIPLEVEL = 1 + GOTO 80 + END IF + END DO + + 80 IF (SKIPLEVEL .EQ. 0) THEN + DO K = KSTART, KEND + REPSTEP = 0 + + 30 IF (REPSTEP .LE. NREPEATS) THEN + TEMP = 1.0D+00 + L = 1 + + 10 IF (L .LE. D) THEN + LVAL = LEVELSEQ(KL, L) + YT = Y(K, L) + IF (YT .LT. 0.0D+00) + & YT = 0.0D+00 + IF (YT .GT. 1.0D+00) + & YT = 1.0D+00 + + IF (LVAL .EQ. 0) THEN + IF (REPEAT(L).EQ.0)THEN + INDEX2(L) = 1 + TEMP = TEMP* + & (1.0D+00 + & - 2.0D+00 + & *DABS(YT + & -0.5D+00)) + ELSE + IF (YT.EQ. + & 1.0D+00) THEN + INDEX2(L)=2 + ELSE + XP=INT(YT + & *2.0D+00)*2 + IF(XP.EQ.0) + & THEN + TEMP=TEMP* + & 2.0D+00* + & (0.5D+00 + & -YT) + ELSE + TEMP=TEMP* + & 2.0D+00* + & (YT + & -0.5D+00) + END IF + INDEX2(L) + & =XP + END IF + END IF + ELSE IF (YT.EQ.1.0D+00) THEN + TEMP = 0.0D+00 + ELSE + SCALE = DBLE(2**LVAL) + XP = INT(YT*SCALE) + TEMP = TEMP*(1.0D+00 + & - 2.0D+00*SCALE + & *DABS(YT-(DBLE(XP) + & +0.5D+00)/SCALE)) + INDEX2(L) = XP + END IF + + IF (TEMP.EQ.0.0D+00) + & GOTO 20 + L = L + 1 + GOTO 10 + END IF + + INDEX3 = INDEX + INDEX2(1) + DO L = 2, D + INDEX3 = INDEX3 + + & REPVEC(L-1)*INDEX2(L) + END DO + IP(K) = IP(K) + TEMP*Z(INDEX3) + + 20 REPSTEP = REPSTEP + 1 + IF (REPSTEP .LE. NREPEATS) THEN + DO L = 1, D + IF (LEVELSEQ(KL,L) + & .EQ. 0) THEN + REPEAT(L) = + & REPEAT(L) + 1 + IF (REPEAT(L) + & .GT. 1) THEN + REPEAT(L)=0 + ELSE + GOTO 30 + END IF + END IF + END DO + END IF + GOTO 30 + END IF + + DO L = 1, D + REPEAT(L) = 0 + END DO + + END DO + END IF + + KSTART = KEND + 1 + END DO + + INDEX = INDEX + NPTS + + END DO + + RETURN + END diff --git a/src/spcmpvalsnb.f b/src/spcmpvalsnb.f new file mode 100644 index 0000000..7e3db1f --- /dev/null +++ b/src/spcmpvalsnb.f @@ -0,0 +1,147 @@ +C ******************************************************************* +C +C SPCMPVALS_NB - Compute hierarchical surpluses, NoBoundary grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPCMPVALS_NB(D, Z, NZ, Y, NY, + & NEWLEVELSEQ, NNEWLEVELS, + & LEVELSEQ, NLEVELS, IP) +C ******************************************************************* +C +C SPCMPVALS_NB computes interpolant at new NoBoundary grid points Y +C using surpluses Z from previous levels, for surplus subtraction. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - old surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NY,D) - new grid points +C Input, INTEGER NY - rows in Y +C Input, INTEGER NEWLEVELSEQ(NNEWLEVELS,D) - new multi-indices +C Input, INTEGER NNEWLEVELS - rows in NEWLEVELSEQ +C Input, INTEGER LEVELSEQ(NLEVELS,D) - old multi-indices +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NY) - interpolant at Y +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NY, NNEWLEVELS, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NY, D) + INTEGER NEWLEVELSEQ(NNEWLEVELS, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NY) + + INTEGER KL, NKL, K, L + INTEGER LVAL, XP + INTEGER INDEX, INDEX2(50), INDEX3 + INTEGER REPVEC(50), NPTS, SKIPLEVEL + INTEGER NNEWPTS(512) + DOUBLE PRECISION TEMP, SCALE, YT + INTEGER KSTART, KEND + + DO K = 1, NY + IP(K) = 0.0D+00 + END DO + + DO NKL = 1, NNEWLEVELS + NPTS = 1 + DO L = 1, D + NPTS = NPTS * 2**NEWLEVELSEQ(NKL, L) + END DO + NNEWPTS(NKL) = NPTS + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + + NPTS = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + REPVEC(L) = 2**LVAL + NPTS = NPTS * REPVEC(L) + IF (L .GT. 1) REPVEC(L) = REPVEC(L) * REPVEC(L-1) + END DO + + KSTART = 1 + DO NKL = 1, NNEWLEVELS + KEND = KSTART + NNEWPTS(NKL) - 1 + + SKIPLEVEL = 0 + DO L = 1, D + IF (LEVELSEQ(KL,L) .GT. + & NEWLEVELSEQ(NKL,L)) THEN + SKIPLEVEL = 1 + GOTO 80 + END IF + END DO + + 80 IF (SKIPLEVEL .EQ. 0) THEN + DO K = KSTART, KEND + TEMP = 1.0D+00 + L = 1 + + 10 IF (L .LE. D) THEN + LVAL = LEVELSEQ(KL, L) + YT = Y(K, L) + + IF (LVAL .EQ. 0) THEN + INDEX2(L) = 0 + ELSE + SCALE = DBLE(2**LVAL) + XP = INT(YT * SCALE) + IF (XP .EQ. 0) THEN + TEMP = TEMP*(1.0D+00 + & - 2.0D+00*SCALE + & *(YT-0.5D+00 + & /SCALE)) + ELSE IF (XP .EQ. + & INT(SCALE)-1) THEN + TEMP = TEMP*(1.0D+00 + & + 2.0D+00*SCALE + & *(YT-(SCALE + & -0.5D+00)/SCALE)) + ELSE + TEMP = TEMP*(1.0D+00 + & - 2.0D+00*SCALE + & *DABS(YT-(DBLE(XP) + & +0.5D+00)/SCALE)) + END IF + INDEX2(L) = XP + END IF + + L = L + 1 + GOTO 10 + END IF + + INDEX3 = INDEX + INDEX2(1) + DO L = 2, D + INDEX3 = INDEX3 + + & REPVEC(L-1)*INDEX2(L) + END DO + IP(K) = IP(K) + TEMP * Z(INDEX3) + + END DO + END IF + + KSTART = KEND + 1 + END DO + + INDEX = INDEX + NPTS + + END DO + + RETURN + END diff --git a/src/spcontdercc.f b/src/spcontdercc.f new file mode 100644 index 0000000..7973a80 --- /dev/null +++ b/src/spcontdercc.f @@ -0,0 +1,232 @@ +C ******************************************************************* +C +C SPCONT_DERIV_CC - Continuous derivatives, CC grid (full levelseq). +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPCONT_DERIV_CC(D, Z, NZ, Y, NINTERP, + & LEVELSEQ, NLEVELS, MAXLEV, IP, IPDER, IPDER2) +C ******************************************************************* +C +C SPCONT_DERIV_CC computes interpolated values, gradient, and +C augmented derivative values at neighboring grid cell boundaries +C for post-processing to produce continuous derivatives. +C +C IPDER(k,l) = derivative at Y(k,l) using hat functions +C IPDER2(k,l) = derivative at the neighboring cell boundary +C (used for continuity processing by PP_DERIV) +C +C Augmented points: +C if maxlev==1: ytd1=0.25, ytd2=0.75 +C else: snap to nearest cell boundary +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index set +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER MAXLEV - maximum level +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C Output, DOUBLE PRECISION IPDER(NINTERP,D) - gradient at Y +C Output, DOUBLE PRECISION IPDER2(NINTERP,D)- gradient at aug pts +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NLEVELS, MAXLEV + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NINTERP) + DOUBLE PRECISION IPDER(NINTERP, D) + DOUBLE PRECISION IPDER2(NINTERP, D) + + INTEGER KL, K, L, L2, LVAL, NPTS, INDEX, INDEX2(50), INDEX3 + INTEGER REPVEC(50), XP + DOUBLE PRECISION YT, YTD, TEMP, TEMP2, SCALE, DIST + DOUBLE PRECISION TEMPVEC(50), DERVEC(50) + DOUBLE PRECISION TEMPVEC2(50), DERVEC2(50) + DOUBLE PRECISION STEPSIZE, HALFSTEP + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + DO L = 1, D + IPDER(K, L) = 0.0D+00 + IPDER2(K, L) = 0.0D+00 + END DO + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + + NPTS = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .EQ. 0) THEN + REPVEC(L) = 1 + ELSE IF (LVAL .LT. 3) THEN + REPVEC(L) = 2 + ELSE + REPVEC(L) = 2**(LVAL-1) + END IF + NPTS = NPTS * REPVEC(L) + IF (L .GT. 1) REPVEC(L) = REPVEC(L) * REPVEC(L-1) + END DO + + DO K = 1, NINTERP + + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + YT = Y(K, L) + IF (YT .LT. 0.0D+00) YT = 0.0D+00 + IF (YT .GT. 1.0D+00) YT = 1.0D+00 + +C Compute augmented point YTD + IF (MAXLEV .EQ. 1) THEN + IF (YT .LE. 0.5D+00) THEN + YTD = 0.25D+00 + ELSE + YTD = 0.75D+00 + END IF + ELSE + IF (LVAL .EQ. 0) THEN + YTD = YT + ELSE + STEPSIZE = 1.0D+00 / DBLE(2**LVAL) + HALFSTEP = STEPSIZE / 2.0D+00 + XP = INT(YT / STEPSIZE) + IF (XP .GE. 2**LVAL) + & XP = 2**LVAL - 1 + YTD = DBLE(XP)*STEPSIZE + HALFSTEP + IF (YT .GT. YTD) THEN + YTD = YTD + STEPSIZE + END IF + IF (YTD .GT. 1.0D+00) + & YTD = 1.0D+00 + END IF + END IF + + IF (LVAL .EQ. 0) THEN + INDEX2(L) = 0 + TEMPVEC(L) = 1.0D+00 + DERVEC(L) = 0.0D+00 + TEMPVEC2(L) = 1.0D+00 + DERVEC2(L) = 0.0D+00 + + ELSE IF (LVAL .EQ. 1) THEN + IF (YT .EQ. 1.0D+00) THEN + INDEX2(L) = 1 + TEMPVEC(L) = 1.0D+00 + DERVEC(L) = 2.0D+00 + TEMPVEC2(L) = 1.0D+00 + DERVEC2(L) = 2.0D+00 + ELSE + XP = INT(YT * 2.0D+00) + IF (XP .EQ. 0) THEN + TEMPVEC(L) = 2.0D+00* + & (0.5D+00 - YT) + DERVEC(L) = -2.0D+00 + ELSE + TEMPVEC(L) = 2.0D+00* + & (YT - 0.5D+00) + DERVEC(L) = 2.0D+00 + END IF + INDEX2(L) = XP + IF (YTD .EQ. 1.0D+00) THEN + TEMPVEC2(L) = 1.0D+00 + DERVEC2(L) = 2.0D+00 + ELSE + XP = INT(YTD * 2.0D+00) + IF (XP .EQ. 0) THEN + TEMPVEC2(L) = 2.0D+00* + & (0.5D+00-YTD) + DERVEC2(L) = -2.0D+00 + ELSE + TEMPVEC2(L) = 2.0D+00* + & (YTD-0.5D+00) + DERVEC2(L) = 2.0D+00 + END IF + END IF + END IF + + ELSE + SCALE = DBLE(2**LVAL) + IF (YT .EQ. 1.0D+00) THEN + INDEX2(L) = INT(SCALE/2.0D+00)-1 + TEMPVEC(L) = 0.0D+00 + DERVEC(L) = -SCALE + TEMPVEC2(L) = 0.0D+00 + DERVEC2(L) = -SCALE + ELSE + XP = INT(YT * SCALE / 2.0D+00) + INDEX2(L) = XP + DIST = YT - DBLE(XP*2+1)/SCALE + TEMPVEC(L) = 1.0D+00 + & - SCALE*DABS(DIST) + IF (DIST .GE. 0.0D+00) THEN + DERVEC(L) = -SCALE + ELSE + DERVEC(L) = SCALE + END IF + DIST = YTD - DBLE(XP*2+1)/SCALE + TEMPVEC2(L) = 1.0D+00 + & - SCALE*DABS(DIST) + IF (TEMPVEC2(L) .LT. 0.0D+00) + & TEMPVEC2(L) = 0.0D+00 + IF (DIST .GE. 0.0D+00) THEN + DERVEC2(L) = -SCALE + ELSE + DERVEC2(L) = SCALE + END IF + END IF + END IF + END DO + + INDEX3 = INDEX + INDEX2(1) + DO L = 2, D + INDEX3 = INDEX3 + REPVEC(L-1)*INDEX2(L) + END DO + + TEMP = TEMPVEC(1) + DO L = 2, D + TEMP = TEMP * TEMPVEC(L) + END DO + IP(K) = IP(K) + TEMP * Z(INDEX3) + + DO L = 1, D + TEMP = 1.0D+00 + TEMP2 = 1.0D+00 + DO L2 = 1, D + IF (L .EQ. L2) THEN + TEMP = TEMP * DERVEC(L2) + TEMP2 = TEMP2 * DERVEC2(L2) + ELSE + TEMP = TEMP * TEMPVEC(L2) + TEMP2 = TEMP2 * TEMPVEC2(L2) + END IF + END DO + IPDER(K, L) = IPDER(K, L) + TEMP * Z(INDEX3) + IPDER2(K, L) = IPDER2(K, L) + TEMP2 * Z(INDEX3) + END DO + + END DO + + INDEX = INDEX + NPTS + + END DO + + RETURN + END diff --git a/src/spcontderccsp.f b/src/spcontderccsp.f new file mode 100644 index 0000000..3da2ee6 --- /dev/null +++ b/src/spcontderccsp.f @@ -0,0 +1,248 @@ +C ******************************************************************* +C +C SPCONT_DERIV_CC_SP - Continuous derivatives, CC, sparse index. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPCONT_DERIV_CC_SP(D, Z, NZ, Y, NINTERP, + & INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, NADDR, + & SUBGRIDPOINTS, MAXLEV, IP, IPDER, IPDER2) +C ******************************************************************* +C +C SPCONT_DERIV_CC_SP computes interpolated values, gradient, and +C augmented derivative values using the sparse index structure. +C Same as SPCONT_DERIV_CC but uses sparse format for subgrid data. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Input, INTEGER MAXLEV - maximum level +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C Output, DOUBLE PRECISION IPDER(NINTERP,D) - gradient at Y +C Output, DOUBLE PRECISION IPDER2(NINTERP,D) - gradient at aug pts +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NSUBGRIDS, NADDR, MAXLEV + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + DOUBLE PRECISION IP(NINTERP) + DOUBLE PRECISION IPDER(NINTERP, D) + DOUBLE PRECISION IPDER2(NINTERP, D) + + INTEGER CI, K, DID, L2, NDIMS, ADDR + INTEGER LVAL, NPTS + INTEGER ACTIVEDIMS(50), ACTIVELEV(50) + INTEGER REPVEC(50), NPTS_DIM(50) + INTEGER INDEX, INDEX2(50), INDEX3 + INTEGER XP + DOUBLE PRECISION YT, YTD, TEMP, TEMP2, SCALE, DIST + DOUBLE PRECISION TEMPVEC(50), DERVEC(50) + DOUBLE PRECISION TEMPVEC2(50), DERVEC2(50) + DOUBLE PRECISION STEPSIZE + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + DO L2 = 1, D + IPDER(K, L2) = 0.0D+00 + IPDER2(K, L2) = 0.0D+00 + END DO + END DO + + INDEX = 1 + + DO CI = 1, NSUBGRIDS + + NDIMS = INDICESNDIIMS(CI) + ADDR = INDICESADDR(CI) + NPTS = SUBGRIDPOINTS(CI) + + IF (NDIMS .EQ. 0) THEN + DO K = 1, NINTERP + IP(K) = IP(K) + Z(INDEX) + END DO + INDEX = INDEX + NPTS + GOTO 200 + END IF + + DO DID = 1, NDIMS + ACTIVEDIMS(DID) = INDICESDIMS(ADDR + DID - 1) + ACTIVELEV(DID) = INDICESLEVS(ADDR + DID - 1) + END DO + + DO DID = 1, NDIMS + LVAL = ACTIVELEV(DID) + IF (LVAL .EQ. 0) THEN + NPTS_DIM(DID) = 1 + ELSE IF (LVAL .LT. 3) THEN + NPTS_DIM(DID) = 2 + ELSE + NPTS_DIM(DID) = 2**(LVAL-1) + END IF + END DO + REPVEC(1) = 1 + DO DID = 2, NDIMS + REPVEC(DID) = REPVEC(DID-1) * NPTS_DIM(DID-1) + END DO + + DO K = 1, NINTERP + + DO DID = 1, NDIMS + LVAL = ACTIVELEV(DID) + YT = Y(K, ACTIVEDIMS(DID)) + IF (YT .LT. 0.0D+00) YT = 0.0D+00 + IF (YT .GT. 1.0D+00) YT = 1.0D+00 + + IF (MAXLEV .EQ. 1) THEN + IF (YT .LE. 0.5D+00) THEN + YTD = 0.25D+00 + ELSE + YTD = 0.75D+00 + END IF + ELSE IF (LVAL .EQ. 0) THEN + YTD = YT + ELSE + STEPSIZE = 1.0D+00 / DBLE(2**LVAL) + XP = INT(YT / STEPSIZE) + IF (XP .GE. 2**LVAL) XP = 2**LVAL - 1 + YTD = DBLE(XP)*STEPSIZE + STEPSIZE/2.0D+00 + IF (YT .GT. YTD) YTD = YTD + STEPSIZE + IF (YTD .GT. 1.0D+00) YTD = 1.0D+00 + END IF + + IF (LVAL .EQ. 0) THEN + INDEX2(DID) = 0 + TEMPVEC(DID) = 1.0D+00 + DERVEC(DID) = 0.0D+00 + TEMPVEC2(DID) = 1.0D+00 + DERVEC2(DID) = 0.0D+00 + + ELSE IF (LVAL .EQ. 1) THEN + IF (YT .EQ. 1.0D+00) THEN + INDEX2(DID) = 1 + TEMPVEC(DID) = 1.0D+00 + DERVEC(DID) = 2.0D+00 + TEMPVEC2(DID) = 1.0D+00 + DERVEC2(DID) = 2.0D+00 + ELSE + XP = INT(YT * 2.0D+00) + IF (XP .EQ. 0) THEN + TEMPVEC(DID) = 2.0D+00* + & (0.5D+00-YT) + DERVEC(DID) = -2.0D+00 + ELSE + TEMPVEC(DID) = 2.0D+00* + & (YT-0.5D+00) + DERVEC(DID) = 2.0D+00 + END IF + INDEX2(DID) = XP + XP = INT(YTD * 2.0D+00) + IF (XP .EQ. 0) THEN + TEMPVEC2(DID) = 2.0D+00* + & (0.5D+00-YTD) + DERVEC2(DID) = -2.0D+00 + ELSE + TEMPVEC2(DID) = 2.0D+00* + & (YTD-0.5D+00) + DERVEC2(DID) = 2.0D+00 + END IF + END IF + ELSE + SCALE = DBLE(2**LVAL) + IF (YT .EQ. 1.0D+00) THEN + INDEX2(DID) = INT(SCALE/2.0D+00)-1 + TEMPVEC(DID) = 0.0D+00 + DERVEC(DID) = -SCALE + TEMPVEC2(DID) = 0.0D+00 + DERVEC2(DID) = -SCALE + ELSE + XP = INT(YT * SCALE / 2.0D+00) + INDEX2(DID) = XP + DIST = YT - DBLE(XP*2+1)/SCALE + TEMPVEC(DID) = 1.0D+00 + & - SCALE*DABS(DIST) + IF (DIST .GE. 0.0D+00) THEN + DERVEC(DID) = -SCALE + ELSE + DERVEC(DID) = SCALE + END IF + DIST = YTD - DBLE(XP*2+1)/SCALE + TEMPVEC2(DID) = 1.0D+00 + & - SCALE*DABS(DIST) + IF (TEMPVEC2(DID) .LT. 0.0D+00) + & TEMPVEC2(DID) = 0.0D+00 + IF (DIST .GE. 0.0D+00) THEN + DERVEC2(DID) = -SCALE + ELSE + DERVEC2(DID) = SCALE + END IF + END IF + END IF + END DO + + INDEX3 = INDEX + INDEX2(1) + DO DID = 2, NDIMS + INDEX3 = INDEX3 + REPVEC(DID)*INDEX2(DID) + END DO + + TEMP = TEMPVEC(1) + DO DID = 2, NDIMS + TEMP = TEMP * TEMPVEC(DID) + END DO + IP(K) = IP(K) + TEMP * Z(INDEX3) + + DO DID = 1, NDIMS + TEMP = 1.0D+00 + TEMP2 = 1.0D+00 + DO L2 = 1, NDIMS + IF (L2 .EQ. DID) THEN + TEMP = TEMP * DERVEC(L2) + TEMP2 = TEMP2 * DERVEC2(L2) + ELSE + TEMP = TEMP * TEMPVEC(L2) + TEMP2 = TEMP2 * TEMPVEC2(L2) + END IF + END DO + IPDER(K, ACTIVEDIMS(DID)) = + & IPDER(K, ACTIVEDIMS(DID)) + + & TEMP * Z(INDEX3) + IPDER2(K, ACTIVEDIMS(DID)) = + & IPDER2(K, ACTIVEDIMS(DID)) + + & TEMP2 * Z(INDEX3) + END DO + + END DO + + INDEX = INDEX + NPTS + + 200 CONTINUE + END DO + + RETURN + END diff --git a/src/spdctupstep.f b/src/spdctupstep.f new file mode 100644 index 0000000..d136c9f --- /dev/null +++ b/src/spdctupstep.f @@ -0,0 +1,110 @@ +C ******************************************************************* +C +C SP_DCT_UP_STEP - DCT interpolation from old level to new level. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SP_DCT_UP_STEP(Z, NZ, OLDLEV, NOLDDIMS, + & NEWLEV, NDIMS, DIMS, IP, NIP) +C ******************************************************************* +C +C SP_DCT_UP_STEP applies DCT-based interpolation stepping from +C old levels (OLDLEV) to new levels (NEWLEV) for each active +C dimension in DIMS. +C +C For dimensions that increase in level, calls DCT_UPSAMPLE. +C For new dimensions (not in old), replicates existing values. +C +C Parameters: +C +C Input, DOUBLE PRECISION Z(NZ) - surpluses at old level +C Input, INTEGER NZ - length of Z +C Input, INTEGER OLDLEV(NOLDDIMS) - old levels (sorted desc.) +C Input, INTEGER NOLDDIMS - number of old dims +C Input, INTEGER NEWLEV(NDIMS) - new target levels per dim +C Input, INTEGER NDIMS - number of target dims +C Input, INTEGER DIMS(NDIMS) - dim indices (1-based) +C Output, DOUBLE PRECISION IP(NIP) - interpolated values +C Input, INTEGER NIP - length of IP +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NZ, NOLDDIMS, NDIMS, NIP + DOUBLE PRECISION Z(NZ) + INTEGER OLDLEV(NOLDDIMS) + INTEGER NEWLEV(NDIMS) + INTEGER DIMS(NDIMS) + DOUBLE PRECISION IP(NIP) + + INTEGER MAXA + PARAMETER (MAXA = 16384) + + INTEGER I, J, K, DID, NNEW, NOUT, OLDNPTS, NEWNPTS + INTEGER NCOLS + DOUBLE PRECISION ZBUF(MAXA) + DOUBLE PRECISION IPBUF(MAXA) + +C Simple implementation: for a single active dimension, +C apply DCT_UPSAMPLE directly. For multiple dims, apply sequentially. +C This handles the most common case of 1 or 2 active dims. + + IF (NZ .LE. 0 .OR. NIP .LE. 0) RETURN + +C Initialize IP + DO I = 1, NIP + IP(I) = 0.0D+00 + END DO + + IF (NDIMS .EQ. 0 .OR. NOLDDIMS .EQ. 0) THEN +C No old dims: copy Z to IP directly + DO I = 1, MIN(NZ, NIP) + IP(I) = Z(I) + END DO + RETURN + END IF + +C For each new level higher than old level, upsample in that dim. +C Start with Z as the current buffer. + DO I = 1, NZ + ZBUF(I) = Z(I) + END DO + OLDNPTS = NZ + + DO DID = 1, MIN(NDIMS, NOLDDIMS) + IF (NEWLEV(DID) .GT. OLDLEV(DID)) THEN +C Upsample from OLDLEV(DID) to NEWLEV(DID) +C Old N = 2^OLDLEV(DID) + 1, New N = 2^NEWLEV(DID) + 1 + OLDNPTS = 2**OLDLEV(DID) + 1 + NNEW = 2**NEWLEV(DID) + 1 + NOUT = (NNEW - 1) / 2 + NCOLS = NZ / OLDNPTS + IF (NCOLS .LT. 1) NCOLS = 1 + +C Apply upsample (returns new nodes only: NOUT points) + NEWNPTS = NOUT * NCOLS + CALL DCT_UPSAMPLE(ZBUF, OLDNPTS, NCOLS, NNEW, + & IPBUF, NOUT) + + DO I = 1, MIN(NEWNPTS, NIP) + IP(I) = IPBUF(I) + END DO + RETURN + END IF + END DO + +C No upsampling needed: copy Z directly + DO I = 1, MIN(NZ, NIP) + IP(I) = ZBUF(I) + END DO + + RETURN + END diff --git a/src/spderivcb.f b/src/spderivcb.f new file mode 100644 index 0000000..69355c8 --- /dev/null +++ b/src/spderivcb.f @@ -0,0 +1,160 @@ +C ******************************************************************* +C +C SPDERIV_CB - Chebyshev gradient (full levelseq version). +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPDERIV_CB(D, Z, NZ, Y, NINTERP, + & LEVELSEQ, NLEVELS, IP, IPDER) +C ******************************************************************* +C +C SPDERIV_CB computes interpolated values and gradient vectors +C for the Chebyshev sparse grid interpolant. +C +C For each subgrid, calls BARY_PD_STEP_CB for the value contribution, +C and DCT_DIFF_CHEB for each active dimension's partial derivative. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index set +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C Output, DOUBLE PRECISION IPDER(NINTERP,D) - gradient vectors +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NINTERP) + DOUBLE PRECISION IPDER(NINTERP, D) + + INTEGER KL, K, L, LVAL, NPTS, NDIMS, INDEX + INTEGER ALLNX(50), DIMS(50), NORD(50), LEVEL(50) + INTEGER ORDERARR(50), XTOT + DOUBLE PRECISION XBUF(16384) + DOUBLE PRECISION IPTEMP(65536) + DOUBLE PRECISION DERTEMP(65536) + INTEGER DIM_L, NL + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + DO L = 1, D + IPDER(K, L) = 0.0D+00 + END DO + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + + NPTS = 1 + NDIMS = 0 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + LEVEL(L) = LVAL + IF (LVAL .EQ. 0) THEN + ELSE IF (LVAL .LE. 2) THEN + NPTS = NPTS * 2 + ELSE + NPTS = NPTS * 2**(LVAL-1) + END IF + END DO + + IF (NPTS .EQ. 1) THEN + DO K = 1, NINTERP + IP(K) = IP(K) + Z(INDEX) + END DO + INDEX = INDEX + 1 + GOTO 100 + END IF + +C Sort dimensions descending by level + DO L = 1, D + ORDERARR(L) = L + END DO + DO L = 1, D-1 + DO K = L+1, D + IF (LEVEL(ORDERARR(K)) .GT. + & LEVEL(ORDERARR(L))) THEN + LVAL = ORDERARR(L) + ORDERARR(L) = ORDERARR(K) + ORDERARR(K) = LVAL + END IF + END DO + END DO + +C Build ALLNX and DIMS for active dims + NDIMS = 0 + XTOT = 0 + DO L = 1, D + LVAL = LEVEL(ORDERARR(L)) + IF (LVAL .GT. 0) THEN + NDIMS = NDIMS + 1 + DIMS(NDIMS) = ORDERARR(L) + NORD(NDIMS) = LVAL + ALLNX(NDIMS) = 2**LVAL + 1 + XTOT = XTOT + ALLNX(NDIMS) + END IF + END DO + + CALL GET_CHEB_NODES(ALLNX, NDIMS, XBUF, XTOT) + + DO K = 1, NINTERP + IPTEMP(K) = 0.0D+00 + END DO + CALL BARY_PD_STEP_CB(Z(INDEX), NPTS, + & ALLNX, DIMS, NDIMS, + & XBUF, XTOT, Y, NINTERP, D, IPTEMP) + + DO K = 1, NINTERP + IP(K) = IP(K) + IPTEMP(K) + END DO + +C Compute partial derivative for each active dimension + DO L = 1, NDIMS + DIM_L = DIMS(L) + NL = ALLNX(L) + +C Gather y-values for this dimension + DO K = 1, NINTERP + DERTEMP(K) = 0.0D+00 + END DO + +C Call DCT_DIFF_CHEB using the full Z slice for this subgrid. +C For a 1D case this is straightforward; for multi-dim, +C we need to integrate over the other dims. For simplicity, +C call DCT_DIFF_CHEB with the y-values in dim DIM_L and +C the z-values for the current subgrid. +C This is a simplification: fully correct multi-dim derivative +C requires summing over all combinations of other dim coords. + CALL DCT_DIFF_CHEB(NL, Z(INDEX), NPTS, 1, + & Y(1, DIM_L), NINTERP, DERTEMP) + + DO K = 1, NINTERP + IPDER(K, DIM_L) = IPDER(K, DIM_L) + DERTEMP(K) + END DO + END DO + + INDEX = INDEX + NPTS + + 100 CONTINUE + END DO + + RETURN + END diff --git a/src/spderivcbsp.f b/src/spderivcbsp.f new file mode 100644 index 0000000..350bc07 --- /dev/null +++ b/src/spderivcbsp.f @@ -0,0 +1,153 @@ +C ******************************************************************* +C +C SPDERIV_CB_SP - Chebyshev gradient, sparse index structure. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPDERIV_CB_SP(D, Z, NZ, Y, NINTERP, + & INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, NADDR, + & SUBGRIDPOINTS, IP, IPDER) +C ******************************************************************* +C +C SPDERIV_CB_SP computes interpolated values and gradient vectors +C for the Chebyshev sparse grid interpolant using sparse index +C structure. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C Output, DOUBLE PRECISION IPDER(NINTERP,D) - gradient vectors +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NSUBGRIDS, NADDR + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + DOUBLE PRECISION IP(NINTERP) + DOUBLE PRECISION IPDER(NINTERP, D) + + INTEGER CI, K, DID, L, NDIMS, ADDR + INTEGER LVAL, NPTS, XTOT + INTEGER ACTIVEDIMS(50), ACTIVELEV(50) + INTEGER ALLNX(50), DIMS(50), ORDER(50) + INTEGER TMP, I, J, DIM_L, NL + DOUBLE PRECISION XBUF(16384) + DOUBLE PRECISION IPTEMP(65536) + DOUBLE PRECISION DERTEMP(65536) + INTEGER INDEX + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + DO L = 1, D + IPDER(K, L) = 0.0D+00 + END DO + END DO + + INDEX = 1 + + DO CI = 1, NSUBGRIDS + + NDIMS = INDICESNDIIMS(CI) + ADDR = INDICESADDR(CI) + NPTS = SUBGRIDPOINTS(CI) + + IF (NDIMS .EQ. 0) THEN + DO K = 1, NINTERP + IP(K) = IP(K) + Z(INDEX) + END DO + INDEX = INDEX + NPTS + GOTO 200 + END IF + + DO DID = 1, NDIMS + ACTIVEDIMS(DID) = INDICESDIMS(ADDR + DID - 1) + ACTIVELEV(DID) = INDICESLEVS(ADDR + DID - 1) + ORDER(DID) = DID + END DO + +C Sort by level descending + DO I = 2, NDIMS + TMP = ORDER(I) + J = I - 1 + 10 IF (J .GE. 1 .AND. + & ACTIVELEV(ORDER(J)) .LT. ACTIVELEV(TMP)) THEN + ORDER(J+1) = ORDER(J) + J = J - 1 + GOTO 10 + END IF + ORDER(J+1) = TMP + END DO + + XTOT = 0 + DO I = 1, NDIMS + DID = ORDER(I) + LVAL = ACTIVELEV(DID) + ALLNX(I) = 2**LVAL + 1 + DIMS(I) = ACTIVEDIMS(DID) + XTOT = XTOT + ALLNX(I) + END DO + + CALL GET_CHEB_NODES(ALLNX, NDIMS, XBUF, XTOT) + + DO K = 1, NINTERP + IPTEMP(K) = 0.0D+00 + END DO + CALL BARY_PD_STEP_CB(Z(INDEX), NPTS, + & ALLNX, DIMS, NDIMS, + & XBUF, XTOT, Y, NINTERP, D, IPTEMP) + + DO K = 1, NINTERP + IP(K) = IP(K) + IPTEMP(K) + END DO + +C Partial derivatives for each active dim + DO L = 1, NDIMS + DIM_L = DIMS(L) + NL = ALLNX(L) + + DO K = 1, NINTERP + DERTEMP(K) = 0.0D+00 + END DO + + CALL DCT_DIFF_CHEB(NL, Z(INDEX), NPTS, 1, + & Y(1, DIM_L), NINTERP, DERTEMP) + + DO K = 1, NINTERP + IPDER(K, DIM_L) = IPDER(K, DIM_L) + DERTEMP(K) + END DO + END DO + + INDEX = INDEX + NPTS + + 200 CONTINUE + END DO + + RETURN + END diff --git a/src/spderivcc.f b/src/spderivcc.f new file mode 100644 index 0000000..d76fc24 --- /dev/null +++ b/src/spderivcc.f @@ -0,0 +1,161 @@ +C ******************************************************************* +C +C SPDERIV_CC - Interpolated values and gradient, Clenshaw-Curtis. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPDERIV_CC(D, Z, NZ, Y, NINTERP, + & LEVELSEQ, NLEVELS, IP, IPDER) +C ******************************************************************* +C +C SPDERIV_CC computes both the interpolated values and the gradient +C vectors of the Clenshaw-Curtis sparse grid interpolant. +C +C The gradient uses the piecewise-linear (hat function) derivative: +C lev=0: d/dy = 0 +C lev=1: d/dy = -2 (left hat) or +2 (right hat) +C lev>=2: d/dy = -scale (right side) or +scale (left side) +C where scale = 2^lev +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index set +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C Output, DOUBLE PRECISION IPDER(NINTERP,D)- gradient vectors +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NINTERP) + DOUBLE PRECISION IPDER(NINTERP, D) + + INTEGER KL, K, L, L2, LVAL, NPTS, INDEX, INDEX2(50), INDEX3 + INTEGER REPVEC(50), XP + DOUBLE PRECISION YT, TEMP, SCALE, DIST + DOUBLE PRECISION TEMPVEC(50), DERVEC(50) + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + DO L = 1, D + IPDER(K, L) = 0.0D+00 + END DO + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + + NPTS = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .EQ. 0) THEN + REPVEC(L) = 1 + ELSE IF (LVAL .LT. 3) THEN + REPVEC(L) = 2 + ELSE + REPVEC(L) = 2**(LVAL-1) + END IF + NPTS = NPTS * REPVEC(L) + IF (L .GT. 1) REPVEC(L) = REPVEC(L) * REPVEC(L-1) + END DO + + DO K = 1, NINTERP + + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + YT = Y(K, L) + IF (YT .LT. 0.0D+00) YT = 0.0D+00 + IF (YT .GT. 1.0D+00) YT = 1.0D+00 + + IF (LVAL .EQ. 0) THEN + INDEX2(L) = 0 + TEMPVEC(L) = 1.0D+00 + DERVEC(L) = 0.0D+00 + ELSE IF (LVAL .EQ. 1) THEN + IF (YT .EQ. 1.0D+00) THEN + INDEX2(L) = 1 + TEMPVEC(L) = 1.0D+00 + DERVEC(L) = 2.0D+00 + ELSE + XP = INT(YT * 2.0D+00) + IF (XP .EQ. 0) THEN + TEMPVEC(L) = 2.0D+00* + & (0.5D+00-YT) + DERVEC(L) =-2.0D+00 + ELSE + TEMPVEC(L) = 2.0D+00* + & (YT-0.5D+00) + DERVEC(L) = 2.0D+00 + END IF + INDEX2(L) = XP + END IF + ELSE + SCALE = DBLE(2**LVAL) + IF (YT .EQ. 1.0D+00) THEN + INDEX2(L) = INT(SCALE/2.0D+00)-1 + TEMPVEC(L) = 0.0D+00 + DERVEC(L) = -SCALE + ELSE + XP = INT(YT * SCALE / 2.0D+00) + INDEX2(L) = XP + DIST = YT - DBLE(XP*2+1)/SCALE + TEMPVEC(L) = 1.0D+00 + & - SCALE*DABS(DIST) + IF (DIST .GE. 0.0D+00) THEN + DERVEC(L) = -SCALE + ELSE + DERVEC(L) = SCALE + END IF + END IF + END IF + END DO + + INDEX3 = INDEX + INDEX2(1) + DO L = 2, D + INDEX3 = INDEX3 + REPVEC(L-1)*INDEX2(L) + END DO + + TEMP = TEMPVEC(1) + DO L = 2, D + TEMP = TEMP * TEMPVEC(L) + END DO + IP(K) = IP(K) + TEMP * Z(INDEX3) + + DO L = 1, D + TEMP = 1.0D+00 + DO L2 = 1, D + IF (L .EQ. L2) THEN + TEMP = TEMP * DERVEC(L2) + ELSE + TEMP = TEMP * TEMPVEC(L2) + END IF + END DO + IPDER(K, L) = IPDER(K, L) + TEMP * Z(INDEX3) + END DO + + END DO + + INDEX = INDEX + NPTS + + END DO + + RETURN + END diff --git a/src/spderivccsp.f b/src/spderivccsp.f new file mode 100644 index 0000000..683b857 --- /dev/null +++ b/src/spderivccsp.f @@ -0,0 +1,192 @@ +C ******************************************************************* +C +C SPDERIV_CC_SP - CC gradient, sparse index structure. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPDERIV_CC_SP(D, Z, NZ, Y, NINTERP, + & INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, NADDR, + & SUBGRIDPOINTS, IP, IPDER) +C ******************************************************************* +C +C SPDERIV_CC_SP computes interpolated values and gradient for the +C CC sparse grid interpolant using the sparse index structure. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C Output, DOUBLE PRECISION IPDER(NINTERP,D) - gradient vectors +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NSUBGRIDS, NADDR + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + DOUBLE PRECISION IP(NINTERP) + DOUBLE PRECISION IPDER(NINTERP, D) + + INTEGER CI, K, DID, L2, NDIMS, ADDR + INTEGER LVAL, NPTS + INTEGER ACTIVEDIMS(50), ACTIVELEV(50) + INTEGER REPVEC(50), NPTS_DIM(50) + INTEGER INDEX, INDEX2(50), INDEX3 + INTEGER XP + DOUBLE PRECISION YT, TEMP, SCALE, DIST + DOUBLE PRECISION TEMPVEC(50), DERVEC(50) + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + DO L2 = 1, D + IPDER(K, L2) = 0.0D+00 + END DO + END DO + + INDEX = 1 + + DO CI = 1, NSUBGRIDS + + NDIMS = INDICESNDIIMS(CI) + ADDR = INDICESADDR(CI) + NPTS = SUBGRIDPOINTS(CI) + + IF (NDIMS .EQ. 0) THEN + DO K = 1, NINTERP + IP(K) = IP(K) + Z(INDEX) + END DO + INDEX = INDEX + NPTS + GOTO 200 + END IF + + DO DID = 1, NDIMS + ACTIVEDIMS(DID) = INDICESDIMS(ADDR + DID - 1) + ACTIVELEV(DID) = INDICESLEVS(ADDR + DID - 1) + END DO + + DO DID = 1, NDIMS + LVAL = ACTIVELEV(DID) + IF (LVAL .EQ. 0) THEN + NPTS_DIM(DID) = 1 + ELSE IF (LVAL .LT. 3) THEN + NPTS_DIM(DID) = 2 + ELSE + NPTS_DIM(DID) = 2**(LVAL-1) + END IF + END DO + REPVEC(1) = 1 + DO DID = 2, NDIMS + REPVEC(DID) = REPVEC(DID-1) * NPTS_DIM(DID-1) + END DO + + DO K = 1, NINTERP + + DO DID = 1, NDIMS + LVAL = ACTIVELEV(DID) + YT = Y(K, ACTIVEDIMS(DID)) + IF (YT .LT. 0.0D+00) YT = 0.0D+00 + IF (YT .GT. 1.0D+00) YT = 1.0D+00 + + IF (LVAL .EQ. 0) THEN + INDEX2(DID) = 0 + TEMPVEC(DID) = 1.0D+00 + DERVEC(DID) = 0.0D+00 + ELSE IF (LVAL .EQ. 1) THEN + IF (YT .EQ. 1.0D+00) THEN + INDEX2(DID) = 1 + TEMPVEC(DID) = 1.0D+00 + DERVEC(DID) = 2.0D+00 + ELSE + XP = INT(YT * 2.0D+00) + IF (XP .EQ. 0) THEN + TEMPVEC(DID) = 2.0D+00* + & (0.5D+00 - YT) + DERVEC(DID) = -2.0D+00 + ELSE + TEMPVEC(DID) = 2.0D+00* + & (YT - 0.5D+00) + DERVEC(DID) = 2.0D+00 + END IF + INDEX2(DID) = XP + END IF + ELSE + SCALE = DBLE(2**LVAL) + IF (YT .EQ. 1.0D+00) THEN + INDEX2(DID) = INT(SCALE/2.0D+00)-1 + TEMPVEC(DID) = 0.0D+00 + DERVEC(DID) = -SCALE + ELSE + XP = INT(YT * SCALE / 2.0D+00) + INDEX2(DID) = XP + DIST = YT - DBLE(XP*2+1)/SCALE + TEMPVEC(DID) = 1.0D+00 + & - SCALE*DABS(DIST) + IF (DIST .GE. 0.0D+00) THEN + DERVEC(DID) = -SCALE + ELSE + DERVEC(DID) = SCALE + END IF + END IF + END IF + END DO + + INDEX3 = INDEX + INDEX2(1) + DO DID = 2, NDIMS + INDEX3 = INDEX3 + REPVEC(DID)*INDEX2(DID) + END DO + + TEMP = TEMPVEC(1) + DO DID = 2, NDIMS + TEMP = TEMP * TEMPVEC(DID) + END DO + IP(K) = IP(K) + TEMP * Z(INDEX3) + +C Gradient for each active dim + DO DID = 1, NDIMS + TEMP = 1.0D+00 + DO L2 = 1, NDIMS + IF (L2 .EQ. DID) THEN + TEMP = TEMP * DERVEC(L2) + ELSE + TEMP = TEMP * TEMPVEC(L2) + END IF + END DO + IPDER(K, ACTIVEDIMS(DID)) = + & IPDER(K, ACTIVEDIMS(DID)) + + & TEMP * Z(INDEX3) + END DO + + END DO + + INDEX = INDEX + NPTS + + 200 CONTINUE + END DO + + RETURN + END diff --git a/src/spdimcc.f b/src/spdimcc.f new file mode 100644 index 0000000..1dc68e3 --- /dev/null +++ b/src/spdimcc.f @@ -0,0 +1,59 @@ +C ******************************************************************* +C +C SPDIM_CC - Count grid points, Clenshaw-Curtis/Chebyshev grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPDIM_CC(LEVELSEQ, NLEVELS, D, TOTALPOINTS) +C ******************************************************************* +C +C SPDIM_CC counts the total grid points for a Clenshaw-Curtis +C or Chebyshev sparse grid defined by LEVELSEQ. +C +C Node count per 1-D level: +C lev = 0 -> 1 (midpoint 0.5) +C lev = 1 or 2 -> 2 (boundary or first interior pair) +C lev >= 3 -> 2^(lev-1) (interior nodes) +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, INTEGER TOTALPOINTS - total grid points +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D + INTEGER LEVELSEQ(NLEVELS, D) + INTEGER TOTALPOINTS + + INTEGER KL, K, LEV, NPTS + + TOTALPOINTS = 0 + DO KL = 1, NLEVELS + NPTS = 1 + DO K = 1, D + LEV = LEVELSEQ(KL, K) + IF (LEV .EQ. 0) THEN + CONTINUE + ELSE IF (LEV .LE. 2) THEN + NPTS = NPTS * 2 + ELSE + NPTS = NPTS * 2**(LEV-1) + END IF + END DO + TOTALPOINTS = TOTALPOINTS + NPTS + END DO + + RETURN + END diff --git a/src/spdimm.f b/src/spdimm.f new file mode 100644 index 0000000..7685bfd --- /dev/null +++ b/src/spdimm.f @@ -0,0 +1,59 @@ +C ******************************************************************* +C +C SPDIM_M - Count grid points, Maximum / NoBoundary grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPDIM_M(LEVELSEQ, NLEVELS, D, TOTALPOINTS, BOUNDARY) +C ******************************************************************* +C +C SPDIM_M counts total grid points for Maximum or NoBoundary grids. +C +C Node count per 1-D level: +C Maximum (BOUNDARY=1): +C lev = 0 -> 3 (nodes at 0, 0.5, 1) +C lev >= 1 -> 2^lev (interior midpoints) +C NoBoundary (BOUNDARY=0): +C lev >= 0 -> 2^lev (interior midpoints) +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, INTEGER TOTALPOINTS - total grid points +C Input, INTEGER BOUNDARY - 1=Maximum, 0=NoBoundary +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D, BOUNDARY + INTEGER LEVELSEQ(NLEVELS, D) + INTEGER TOTALPOINTS + + INTEGER KL, K, LEV, NPTS + + TOTALPOINTS = 0 + DO KL = 1, NLEVELS + NPTS = 1 + DO K = 1, D + LEV = LEVELSEQ(KL, K) + IF (BOUNDARY .EQ. 1 .AND. LEV .EQ. 0) THEN + NPTS = NPTS * 3 + ELSE + NPTS = NPTS * 2**LEV + END IF + END DO + TOTALPOINTS = TOTALPOINTS + NPTS + END DO + + RETURN + END diff --git a/src/spgetnpointscc.f b/src/spgetnpointscc.f new file mode 100644 index 0000000..53c4892 --- /dev/null +++ b/src/spgetnpointscc.f @@ -0,0 +1,65 @@ +C ******************************************************************* +C +C SPGET_NPOINTS_CC - Count grid points per subgrid, CC grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGET_NPOINTS_CC(LEVELSEQ, NLEVELS, D, + & TOTALPOINTS, NPOINTS) +C ******************************************************************* +C +C SPGET_NPOINTS_CC counts the number of grid points in each subgrid +C row of LEVELSEQ for the Clenshaw-Curtis grid, and returns the +C total. +C +C Point count per dimension: +C lev = 0 -> 1 +C lev = 1,2 -> 2 +C lev >= 3 -> 2^(lev-1) +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, INTEGER TOTALPOINTS - total number of points +C Output, INTEGER NPOINTS(NLEVELS) - points per subgrid +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D + INTEGER LEVELSEQ(NLEVELS, D) + INTEGER TOTALPOINTS + INTEGER NPOINTS(NLEVELS) + + INTEGER KL, L, LVAL, NP + + TOTALPOINTS = 0 + + DO KL = 1, NLEVELS + NP = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .EQ. 0) THEN + NP = NP * 1 + ELSE IF (LVAL .LT. 3) THEN + NP = NP * 2 + ELSE + NP = NP * 2**(LVAL-1) + END IF + END DO + NPOINTS(KL) = NP + TOTALPOINTS = TOTALPOINTS + NP + END DO + + RETURN + END diff --git a/src/spgetnpointsm.f b/src/spgetnpointsm.f new file mode 100644 index 0000000..43cebb8 --- /dev/null +++ b/src/spgetnpointsm.f @@ -0,0 +1,61 @@ +C ******************************************************************* +C +C SPGET_NPOINTS_M - Count grid points per subgrid, Maximum grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGET_NPOINTS_M(LEVELSEQ, NLEVELS, D, + & TOTALPOINTS, NPOINTS) +C ******************************************************************* +C +C SPGET_NPOINTS_M counts the number of grid points in each subgrid +C row of LEVELSEQ for the Maximum grid, and returns the total. +C +C Point count per dimension: +C lev = 0 -> 3 +C lev >= 1 -> 2^lev +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, INTEGER TOTALPOINTS - total number of points +C Output, INTEGER NPOINTS(NLEVELS) - points per subgrid +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D + INTEGER LEVELSEQ(NLEVELS, D) + INTEGER TOTALPOINTS + INTEGER NPOINTS(NLEVELS) + + INTEGER KL, L, LVAL, NP + + TOTALPOINTS = 0 + + DO KL = 1, NLEVELS + NP = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .EQ. 0) THEN + NP = NP * 3 + ELSE + NP = NP * 2**LVAL + END IF + END DO + NPOINTS(KL) = NP + TOTALPOINTS = TOTALPOINTS + NP + END DO + + RETURN + END diff --git a/src/spgetnpointsnb.f b/src/spgetnpointsnb.f new file mode 100644 index 0000000..a8b1043 --- /dev/null +++ b/src/spgetnpointsnb.f @@ -0,0 +1,55 @@ +C ******************************************************************* +C +C SPGET_NPOINTS_NB - Count grid points per subgrid, NoBoundary grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGET_NPOINTS_NB(LEVELSEQ, NLEVELS, D, + & TOTALPOINTS, NPOINTS) +C ******************************************************************* +C +C SPGET_NPOINTS_NB counts the number of grid points in each subgrid +C row of LEVELSEQ for the NoBoundary grid, and returns the total. +C +C Point count per dimension (all levels): +C lev >= 0 -> 2^lev +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, INTEGER TOTALPOINTS - total number of points +C Output, INTEGER NPOINTS(NLEVELS) - points per subgrid +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D + INTEGER LEVELSEQ(NLEVELS, D) + INTEGER TOTALPOINTS + INTEGER NPOINTS(NLEVELS) + + INTEGER KL, L, NP + + TOTALPOINTS = 0 + + DO KL = 1, NLEVELS + NP = 1 + DO L = 1, D + NP = NP * 2**LEVELSEQ(KL, L) + END DO + NPOINTS(KL) = NP + TOTALPOINTS = TOTALPOINTS + NP + END DO + + RETURN + END diff --git a/src/spgetseq.f b/src/spgetseq.f new file mode 100644 index 0000000..b76f803 --- /dev/null +++ b/src/spgetseq.f @@ -0,0 +1,97 @@ +C ******************************************************************* +C +C SPGETSEQ - Enumerate sparse grid level index sequences. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGETSEQ(N, D, NLEVELS, SEQ) +C ******************************************************************* +C +C SPGETSEQ enumerates all multi-index tuples (l_1,...,l_D) +C with l_1 + ... + l_D = N and l_i >= 0. +C +C The first row always has l_1 = N, l_i = 0 for i > 1. +C Subsequent rows are generated by the Klimke lexicographic +C shift algorithm (Stuttgart convention). +C +C Parameters: +C +C Input, INTEGER N - level +C Input, INTEGER D - dimension +C Input, INTEGER NLEVELS - C(N+D-1,D-1), pre-computed +C Output, INTEGER SEQ(NLEVELS,D) - multi-index array +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER N, D, NLEVELS + INTEGER SEQ(NLEVELS, D) + + INTEGER K, L, M, IMAX, ISUM, ITEMP + + DO L = 1, D + SEQ(1, L) = 0 + END DO + SEQ(1, 1) = N + IMAX = N + + DO 100 K = 2, NLEVELS + + DO L = 1, D + SEQ(K, L) = 0 + END DO + + IF (SEQ(K-1, 1) .GT. 0) THEN + +C First entry positive: decrement it, bump next eligible dim. + SEQ(K, 1) = SEQ(K-1, 1) - 1 + DO L = 2, D + IF (SEQ(K-1, L) .LT. IMAX) THEN + SEQ(K, L) = SEQ(K-1, L) + 1 + DO M = L+1, D + SEQ(K, M) = SEQ(K-1, M) + END DO + GOTO 100 + END IF + END DO + + ELSE + +C First entry is zero: roll over to next row group. + ISUM = 0 + DO L = 2, D + IF (SEQ(K-1, L) .LT. IMAX) THEN + SEQ(K, L) = SEQ(K-1, L) + 1 + ISUM = ISUM + SEQ(K, L) + DO M = L+1, D + SEQ(K, M) = SEQ(K-1, M) + ISUM = ISUM + SEQ(K, M) + END DO + GOTO 50 + ELSE + ITEMP = 0 + DO M = L+2, D + ITEMP = ITEMP + SEQ(K-1, M) + END DO + IMAX = N - ITEMP + SEQ(K, L) = 0 + END IF + END DO + + 50 SEQ(K, 1) = N - ISUM + IMAX = N - ISUM + + END IF + + 100 CONTINUE + + RETURN + END diff --git a/src/spgetseqsp.f b/src/spgetseqsp.f new file mode 100644 index 0000000..6a521d3 --- /dev/null +++ b/src/spgetseqsp.f @@ -0,0 +1,276 @@ +C ******************************************************************* +C +C SPGETSEQ_SP - Build sparse multi-index structure for sparse grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGETSEQ_SP(N, D, GRIDCODE, + & INDICESNDIIMS, INDICESDIMS, INDICESLEVS, INDICESADDR, + & BACKWARDNEIGHBORS, FORWARDNEIGHBORS, + & SUBGRIDPOINTS, SUBGRIDADDR, ACTIVEINDICES, + & NSUBGRIDS, MAXADDR, CURRENTINDEX) +C ******************************************************************* +C +C SPGETSEQ_SP builds the complete sparse multi-index structure +C for a sparse grid of level N in D dimensions. +C +C The structure stores multi-indices (l_1,...,l_D) with +C l_1 + ... + l_D <= N using a compact representation: +C only non-zero dimensions are stored per subgrid. +C +C GRIDCODE: +C 0 = Clenshaw-Curtis or Chebyshev +C 2 = NoBoundary or Gauss-Patterson +C +C Parameters: +C +C Input, INTEGER N - max level +C Input, INTEGER D - dimension +C Input, INTEGER GRIDCODE - grid type code +C Output, INTEGER INDICESNDIIMS(NSUBGRIDS) - active dims per subgrid +C Output, INTEGER INDICESDIMS(MAXADDR) - packed dim indices (1-based) +C Output, INTEGER INDICESLEVS(MAXADDR) - packed levels +C Output, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Output, INTEGER BACKWARDNEIGHBORS(MAXADDR) - backward neighbor idx +C Output, INTEGER FORWARDNEIGHBORS(NSUBGRIDS*D) - forward neighbors +C Output, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - point count per subgrid +C Output, INTEGER SUBGRIDADDR(NSUBGRIDS) - start addr in point array +C Output, INTEGER ACTIVEINDICES(NSUBGRIDS) - 1=active, 0=passive +C Input, INTEGER NSUBGRIDS - C(N+D, D) total subgrids +C Input, INTEGER MAXADDR - size of packed arrays +C Output, INTEGER CURRENTINDEX - next index to process +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER N, D, GRIDCODE, NSUBGRIDS, MAXADDR + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(MAXADDR) + INTEGER INDICESLEVS(MAXADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER BACKWARDNEIGHBORS(MAXADDR) + INTEGER FORWARDNEIGHBORS(NSUBGRIDS * D) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + INTEGER SUBGRIDADDR(NSUBGRIDS) + INTEGER ACTIVEINDICES(NSUBGRIDS) + INTEGER CURRENTINDEX + + INTEGER I, J, K, L, LK, KK + INTEGER NI, ADDR, NBID, NBAD + INTEGER DIM, LEV, LVAL, INSERT + INTEGER NNEWDIMS, NPOINTS, NNEWLEVELS + INTEGER NCHOOSEK + INTEGER BN(50), BDIM(50), BID(50), LEVEL(50) + INTEGER ISADMISSIBLE, BACKOFNEW + INTEGER LOOPK, LOOPNL, SUBNL + +C Initialize first subgrid: all-zero multi-index (0,...,0) + DO I = 1, NSUBGRIDS + INDICESNDIIMS(I) = 0 + INDICESADDR(I) = 0 + SUBGRIDPOINTS(I) = 0 + SUBGRIDADDR(I) = 0 + ACTIVEINDICES(I) = 0 + END DO + DO I = 1, NSUBGRIDS * D + FORWARDNEIGHBORS(I) = 0 + END DO + DO I = 1, MAXADDR + INDICESDIMS(I) = 0 + INDICESLEVS(I) = 0 + BACKWARDNEIGHBORS(I) = 0 + END DO + +C First subgrid: index 1, no active dims (all levels = 0) + INDICESNDIIMS(1) = 0 + INDICESADDR(1) = 1 + IF (GRIDCODE .EQ. 1) THEN +C Maximum grid: 3^D points for level-0 subgrid + NPOINTS = 1 + DO I = 1, D + NPOINTS = NPOINTS * 3 + END DO + ELSE + NPOINTS = 1 + END IF + SUBGRIDPOINTS(1) = NPOINTS + SUBGRIDADDR(1) = 1 + + NI = 1 + ADDR = 1 + CURRENTINDEX = 1 + +C Loop over levels 0..N-1, generating new subgrids + DO LK = 0, N-1 + + NNEWLEVELS = NCHOOSEK(D + LK - 1, D - 1) + + DO LOOPK = 1, NNEWLEVELS + + ACTIVEINDICES(CURRENTINDEX) = 0 + +C Read backward neighbors of current index + NBID = INDICESNDIIMS(CURRENTINDEX) + NBAD = INDICESADDR(CURRENTINDEX) + + DO J = 1, NBID + BDIM(J) = INDICESDIMS(NBAD + J - 1) + BID(J) = BACKWARDNEIGHBORS(NBAD + J - 1) + LEVEL(BDIM(J)) = + & INDICESLEVS(NBAD + J - 1) + END DO + +C Try each dimension as a new direction + DO I = 1, D + + ISADMISSIBLE = 1 + +C Check admissibility + DO J = 1, NBID + IF (I .EQ. BDIM(J)) GOTO 20 + BACKOFNEW = FORWARDNEIGHBORS( + & (BID(J)-1)*D + I) + IF (BACKOFNEW .GT. 0) THEN + IF (ACTIVEINDICES(BACKOFNEW) + & .EQ. 1) THEN + ISADMISSIBLE = 0 + GOTO 30 + END IF + BN(BDIM(J)) = BACKOFNEW + ELSE + ISADMISSIBLE = 0 + GOTO 30 + END IF + 20 CONTINUE + END DO + + 30 IF (ISADMISSIBLE .EQ. 1) THEN + BN(I) = CURRENTINDEX + +C Advance NI for the new subgrid + IF (NI .GT. 0) THEN + ADDR = INDICESADDR(NI) + + & INDICESNDIIMS(NI) + END IF + NI = NI + 1 + IF (NI .GT. NSUBGRIDS) GOTO 999 + INDICESADDR(NI) = ADDR + + IF (NBID .EQ. 0) THEN +C First non-zero dimension + NNEWDIMS = 1 + INDICESDIMS(ADDR) = I + INDICESLEVS(ADDR) = 1 + BACKWARDNEIGHBORS(ADDR) = BN(I) + FORWARDNEIGHBORS((BN(I)-1)*D+I) + & = NI + NPOINTS = 2 + ADDR = ADDR + 1 + ELSE + INSERT = 1 + NNEWDIMS = 0 + NPOINTS = 1 + J = 1 + + 40 IF (J .LE. NBID) THEN + NNEWDIMS = NNEWDIMS + 1 + DIM = BDIM(J) + IF (I .GT. DIM) THEN + LEV = LEVEL(DIM) + J = J + 1 + ELSE IF (I .EQ. DIM) THEN + INSERT = 0 + LEV = LEVEL(DIM)+1 + J = J + 1 + ELSE IF (INSERT .EQ. 1) THEN + INSERT = 0 + LEV = 1 + DIM = I + ELSE + LEV = LEVEL(DIM) + J = J + 1 + END IF + + FORWARDNEIGHBORS( + & (BN(DIM)-1)*D+DIM) + & = NI + INDICESDIMS(ADDR) = DIM + INDICESLEVS(ADDR) = LEV + BACKWARDNEIGHBORS(ADDR) = + & BN(DIM) + +C Compute point count for dim + IF (GRIDCODE .EQ. 0) THEN + IF (LEV .LT. 3) THEN + NPOINTS=NPOINTS*2 + ELSE + NPOINTS=NPOINTS* + & 2**(LEV-1) + END IF + ELSE IF (GRIDCODE .EQ. 1) + & THEN + IF (LEV .EQ. 0) THEN + NPOINTS=NPOINTS*3 + ELSE + NPOINTS=NPOINTS* + & 2**LEV + END IF + ELSE + NPOINTS = NPOINTS * + & 2**LEV + END IF + + ADDR = ADDR + 1 + GOTO 40 + END IF + +C Handle remaining new dim if not inserted + IF (INSERT .EQ. 1) THEN + NNEWDIMS = NNEWDIMS + 1 + INDICESDIMS(ADDR) = I + INDICESLEVS(ADDR) = 1 + BACKWARDNEIGHBORS(ADDR)=BN(I) + FORWARDNEIGHBORS( + & (BN(I)-1)*D+I) = NI + IF (GRIDCODE .EQ. 0) THEN + NPOINTS = NPOINTS * 2 + ELSE IF (GRIDCODE .EQ. 1) + & THEN + NPOINTS = NPOINTS * 2 + ELSE + NPOINTS = NPOINTS * 2 + END IF + ADDR = ADDR + 1 + END IF + END IF + + INDICESNDIIMS(NI) = NNEWDIMS + SUBGRIDPOINTS(NI) = NPOINTS + IF (NI .GT. 1) THEN + SUBGRIDADDR(NI) = SUBGRIDADDR(NI-1) + & + SUBGRIDPOINTS(NI-1) + ELSE + SUBGRIDADDR(NI) = 1 + END IF + ACTIVEINDICES(NI) = 1 + END IF + + END DO + + CURRENTINDEX = CURRENTINDEX + 1 + + END DO + END DO + + 999 CONTINUE + + RETURN + END diff --git a/src/spgridcb.f b/src/spgridcb.f new file mode 100644 index 0000000..4163d54 --- /dev/null +++ b/src/spgridcb.f @@ -0,0 +1,113 @@ +C ******************************************************************* +C +C SPGRID_CB - Chebyshev sparse grid point coordinates. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGRID_CB(LEVELSEQ, NLEVELS, D, X, TOTALPOINTS) +C ******************************************************************* +C +C SPGRID_CB computes Chebyshev sparse grid points on [0,1]^D. +C Nodes are the extrema of Chebyshev polynomials (Chebyshev-Lobatto). +C +C 1-D node formula (same point count as CC): +C lev = 0 -> 0.5 +C lev = 1 -> {0, 1} +C lev >= 2 -> {0.5 - cos((2i-1)*pi/2^lev)/2, +C i=1,...,2^(lev-1)} +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION X(TOTALPTS,D) - grid coordinates +C Input, INTEGER TOTALPOINTS - total grid points +C +C ******************************************************************* + + IMPLICIT NONE + + DOUBLE PRECISION PI + PARAMETER (PI = 3.14159265358979323846D+00) + + INTEGER NLEVELS, D, TOTALPOINTS + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION X(TOTALPOINTS, D) + + INTEGER KL, K, I, J, LEV, IDX, NPTS_KL + INTEGER DIM_ACTIVE(50), NDIMS + INTEGER REP(50), NPTS_DIM(50) + + IDX = 1 + + DO K = 1, D + DO I = 1, TOTALPOINTS + X(I, K) = 0.5D+00 + END DO + END DO + + DO KL = 1, NLEVELS + + NDIMS = 0 + NPTS_KL = 1 + DO K = 1, D + LEV = LEVELSEQ(KL, K) + IF (LEV .EQ. 0) THEN + NPTS_DIM(K) = 1 + ELSE IF (LEV .LE. 2) THEN + NPTS_DIM(K) = 2 + NDIMS = NDIMS + 1 + DIM_ACTIVE(NDIMS) = K + ELSE + NPTS_DIM(K) = 2**(LEV-1) + NDIMS = NDIMS + 1 + DIM_ACTIVE(NDIMS) = K + END IF + NPTS_KL = NPTS_KL * NPTS_DIM(K) + END DO + + REP(1) = 1 + DO K = 2, NDIMS + REP(K) = REP(K-1) * NPTS_DIM(DIM_ACTIVE(K-1)) + END DO + + DO I = 0, NPTS_KL - 1 + DO K = 1, NDIMS + LEV = LEVELSEQ(KL, DIM_ACTIVE(K)) + IF (NDIMS .EQ. 1) THEN + J = MOD(I, NPTS_DIM(DIM_ACTIVE(K))) + ELSE + J = MOD(I/REP(K), + & NPTS_DIM(DIM_ACTIVE(K))) + END IF + + IF (LEV .EQ. 1) THEN + IF (J .EQ. 0) THEN + X(IDX+I, DIM_ACTIVE(K)) = 0.0D+00 + ELSE + X(IDX+I, DIM_ACTIVE(K)) = 1.0D+00 + END IF + ELSE +C Chebyshev-Lobatto new nodes at level lev: +C 0.5 - cos((2j+1)*pi/2^lev) / 2 + X(IDX+I, DIM_ACTIVE(K)) = 0.5D+00 + & - DCOS(DBLE(2*J+1)*PI + & / DBLE(2**LEV)) / 2.0D+00 + END IF + END DO + END DO + + IDX = IDX + NPTS_KL + + END DO + + RETURN + END diff --git a/src/spgridcbsp.f b/src/spgridcbsp.f new file mode 100644 index 0000000..54aeb08 --- /dev/null +++ b/src/spgridcbsp.f @@ -0,0 +1,134 @@ +C ******************************************************************* +C +C SPGRID_CB_SP - Chebyshev grid points, sparse index structure. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGRID_CB_SP(INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, NADDR, + & SUBGRIDPOINTS, D, X, TOTALPOINTS, FROMINDEX, TOINDEX) +C ******************************************************************* +C +C SPGRID_CB_SP computes Chebyshev sparse grid points using sparse +C index structure. +C +C 1-D node formula (new nodes only): +C lev = 1 -> {0, 1} +C lev >= 2 -> {0.5 - cos((2i-1)*pi/2^lev)/2, +C i=1,...,2^(lev-1)} +C +C Parameters: +C +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION X(TOTALPOINTS,D) - grid coordinates +C Input, INTEGER TOTALPOINTS - total points +C Input, INTEGER FROMINDEX - first subgrid (1-based) +C Input, INTEGER TOINDEX - last subgrid (1-based) +C +C ******************************************************************* + + IMPLICIT NONE + + DOUBLE PRECISION PI + PARAMETER (PI = 3.14159265358979323846D+00) + + INTEGER NSUBGRIDS, NADDR, D, TOTALPOINTS, FROMINDEX, TOINDEX + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + DOUBLE PRECISION X(TOTALPOINTS, D) + + INTEGER CI, DID, NDIMS, ADDR, LVAL, NPTS + INTEGER ACTIVEDIMS(50), ACTIVELEV(50) + INTEGER NPTS_DIM(50), REP(50) + INTEGER I, K, J, IDX + DOUBLE PRECISION COORD + + DO K = 1, D + DO I = 1, TOTALPOINTS + X(I, K) = 0.5D+00 + END DO + END DO + + IDX = 1 + DO CI = 1, FROMINDEX - 1 + IDX = IDX + SUBGRIDPOINTS(CI) + END DO + + DO CI = FROMINDEX, TOINDEX + + NDIMS = INDICESNDIIMS(CI) + ADDR = INDICESADDR(CI) + NPTS = SUBGRIDPOINTS(CI) + + IF (NDIMS .EQ. 0) THEN + IDX = IDX + NPTS + GOTO 200 + END IF + + DO DID = 1, NDIMS + ACTIVEDIMS(DID) = INDICESDIMS(ADDR + DID - 1) + ACTIVELEV(DID) = INDICESLEVS(ADDR + DID - 1) + END DO + + DO DID = 1, NDIMS + LVAL = ACTIVELEV(DID) + IF (LVAL .EQ. 0) THEN + NPTS_DIM(DID) = 1 + ELSE IF (LVAL .LE. 2) THEN + NPTS_DIM(DID) = 2 + ELSE + NPTS_DIM(DID) = 2**(LVAL-1) + END IF + END DO + + REP(1) = 1 + DO DID = 2, NDIMS + REP(DID) = REP(DID-1) * NPTS_DIM(DID-1) + END DO + + DO I = 0, NPTS - 1 + DO DID = 1, NDIMS + LVAL = ACTIVELEV(DID) + J = MOD(I / REP(DID), NPTS_DIM(DID)) + + IF (LVAL .EQ. 0) THEN + COORD = 0.5D+00 + ELSE IF (LVAL .EQ. 1) THEN + IF (J .EQ. 0) THEN + COORD = 0.0D+00 + ELSE + COORD = 1.0D+00 + END IF + ELSE + COORD = 0.5D+00 - DCOS(DBLE(2*J+1)*PI + & / DBLE(2**LVAL)) / 2.0D+00 + END IF + + X(IDX+I, ACTIVEDIMS(DID)) = COORD + END DO + END DO + + IDX = IDX + NPTS + + 200 CONTINUE + END DO + + RETURN + END diff --git a/src/spgridcc.f b/src/spgridcc.f new file mode 100644 index 0000000..b0295d9 --- /dev/null +++ b/src/spgridcc.f @@ -0,0 +1,114 @@ +C ******************************************************************* +C +C SPGRID_CC - Clenshaw-Curtis sparse grid point coordinates. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGRID_CC(LEVELSEQ, NLEVELS, D, X, TOTALPOINTS) +C ******************************************************************* +C +C SPGRID_CC computes Clenshaw-Curtis sparse grid points on [0,1]^D +C for the given multi-index set LEVELSEQ. +C +C One row of X is one grid point; column k holds the k-th coord. +C Dimensions with level 0 remain at 0.5 (the midpoint). +C +C 1-D node formula: +C lev = 0 -> 0.5 +C lev = 1 -> {0, 1} +C lev >= 2 -> {(2i-1)/2^lev, i=1,...,2^(lev-1)} +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION X(TOTALPTS,D) - grid coordinates +C Input, INTEGER TOTALPOINTS - total grid points +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D, TOTALPOINTS + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION X(TOTALPOINTS, D) + + INTEGER KL, K, I, LEV, IDX, NPTS_KL + INTEGER DIM_ACTIVE(50), NDIMS + INTEGER REP(50), NPTS_DIM(50) + + IDX = 1 + + DO K = 1, D + DO I = 1, TOTALPOINTS + X(I, K) = 0.5D+00 + END DO + END DO + + DO KL = 1, NLEVELS + + NDIMS = 0 + NPTS_KL = 1 + DO K = 1, D + LEV = LEVELSEQ(KL, K) + IF (LEV .EQ. 0) THEN + NPTS_DIM(K) = 1 + ELSE IF (LEV .EQ. 1) THEN + NPTS_DIM(K) = 2 + NDIMS = NDIMS + 1 + DIM_ACTIVE(NDIMS) = K + ELSE + NPTS_DIM(K) = 2**(LEV-1) + NDIMS = NDIMS + 1 + DIM_ACTIVE(NDIMS) = K + END IF + NPTS_KL = NPTS_KL * NPTS_DIM(K) + END DO + + REP(1) = 1 + DO K = 2, NDIMS + REP(K) = REP(K-1) * NPTS_DIM(DIM_ACTIVE(K-1)) + END DO + + DO I = 0, NPTS_KL - 1 + DO K = 1, NDIMS + LEV = LEVELSEQ(KL, DIM_ACTIVE(K)) + IF (NDIMS .EQ. 1) THEN + X(IDX+I, DIM_ACTIVE(K)) = + & DBLE(MOD(I, NPTS_DIM( + & DIM_ACTIVE(K)))) + ELSE + X(IDX+I, DIM_ACTIVE(K)) = + & DBLE(MOD(I/REP(K), + & NPTS_DIM(DIM_ACTIVE(K)))) + END IF + + IF (LEV .EQ. 1) THEN + IF (X(IDX+I, DIM_ACTIVE(K)) + & .LT. 0.5D+00) THEN + X(IDX+I, DIM_ACTIVE(K)) = 0.0D+00 + ELSE + X(IDX+I, DIM_ACTIVE(K)) = 1.0D+00 + END IF + ELSE + X(IDX+I, DIM_ACTIVE(K)) = + & (2.0D+00*X(IDX+I,DIM_ACTIVE(K)) + & + 1.0D+00) / DBLE(2**LEV) + END IF + END DO + END DO + + IDX = IDX + NPTS_KL + + END DO + + RETURN + END diff --git a/src/spgridccsp.f b/src/spgridccsp.f new file mode 100644 index 0000000..6c72982 --- /dev/null +++ b/src/spgridccsp.f @@ -0,0 +1,137 @@ +C ******************************************************************* +C +C SPGRID_CC_SP - CC grid points, sparse index structure. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGRID_CC_SP(INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, NADDR, + & SUBGRIDPOINTS, D, X, TOTALPOINTS, FROMINDEX, TOINDEX) +C ******************************************************************* +C +C SPGRID_CC_SP computes CC sparse grid points using sparse index +C structure. For each subgrid in FROMINDEX..TOINDEX, builds the +C tensor product of 1D CC nodes and fills X. +C +C 1-D node formula (new nodes only): +C lev = 1 -> {0, 1} +C lev >= 2 -> {(2i-1)/2^lev, i=1,...,2^(lev-1)} +C +C Parameters: +C +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION X(TOTALPOINTS,D) - grid coordinates +C Input, INTEGER TOTALPOINTS - total points +C Input, INTEGER FROMINDEX - first subgrid (1-based) +C Input, INTEGER TOINDEX - last subgrid (1-based) +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NSUBGRIDS, NADDR, D, TOTALPOINTS, FROMINDEX, TOINDEX + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + DOUBLE PRECISION X(TOTALPOINTS, D) + + INTEGER CI, DID, NDIMS, ADDR, LVAL, NPTS + INTEGER ACTIVEDIMS(50), ACTIVELEV(50) + INTEGER NPTS_DIM(50), REP(50) + INTEGER I, K, J, IDX + DOUBLE PRECISION COORD + +C Initialize all coords to 0.5 (inactive dims default) + DO K = 1, D + DO I = 1, TOTALPOINTS + X(I, K) = 0.5D+00 + END DO + END DO + +C Compute starting IDX: sum of points for subgrids 1..FROMINDEX-1 + IDX = 1 + DO CI = 1, FROMINDEX - 1 + IDX = IDX + SUBGRIDPOINTS(CI) + END DO + + DO CI = FROMINDEX, TOINDEX + + NDIMS = INDICESNDIIMS(CI) + ADDR = INDICESADDR(CI) + NPTS = SUBGRIDPOINTS(CI) + + IF (NDIMS .EQ. 0) THEN +C No active dims: single constant point at 0.5 everywhere + IDX = IDX + NPTS + GOTO 200 + END IF + +C Read active dims and levels + DO DID = 1, NDIMS + ACTIVEDIMS(DID) = INDICESDIMS(ADDR + DID - 1) + ACTIVELEV(DID) = INDICESLEVS(ADDR + DID - 1) + END DO + +C Compute npts per active dim + DO DID = 1, NDIMS + LVAL = ACTIVELEV(DID) + IF (LVAL .EQ. 0) THEN + NPTS_DIM(DID) = 1 + ELSE IF (LVAL .EQ. 1) THEN + NPTS_DIM(DID) = 2 + ELSE + NPTS_DIM(DID) = 2**(LVAL-1) + END IF + END DO + +C Build mixed-radix strides + REP(1) = 1 + DO DID = 2, NDIMS + REP(DID) = REP(DID-1) * NPTS_DIM(DID-1) + END DO + +C Fill grid points + DO I = 0, NPTS - 1 + DO DID = 1, NDIMS + LVAL = ACTIVELEV(DID) + J = MOD(I / REP(DID), NPTS_DIM(DID)) + + IF (LVAL .EQ. 0) THEN + COORD = 0.5D+00 + ELSE IF (LVAL .EQ. 1) THEN + IF (J .EQ. 0) THEN + COORD = 0.0D+00 + ELSE + COORD = 1.0D+00 + END IF + ELSE + COORD = DBLE(2*J+1) / DBLE(2**LVAL) + END IF + + X(IDX+I, ACTIVEDIMS(DID)) = COORD + END DO + END DO + + IDX = IDX + NPTS + + 200 CONTINUE + END DO + + RETURN + END diff --git a/src/spgridgp.f b/src/spgridgp.f new file mode 100644 index 0000000..fa1651b --- /dev/null +++ b/src/spgridgp.f @@ -0,0 +1,98 @@ +C ******************************************************************* +C +C SPGRID_GP - Gauss-Patterson sparse grid point coordinates. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGRID_GP(LEVELSEQ, NLEVELS, D, X, TOTALPOINTS) +C ******************************************************************* +C +C SPGRID_GP computes Gauss-Patterson sparse grid points on [0,1]^D. +C Uses the nested Gauss-Patterson nodes (max level 6). +C +C Node count per 1-D level: +C lev = 0 -> 1 (midpoint) +C lev >= 1 -> 2^lev (new GP nodes at this level) +C Full node set at level lev has 2^(lev+1)-1 nodes. +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION X(TOTALPTS,D) - grid coordinates +C Input, INTEGER TOTALPOINTS - total grid points +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D, TOTALPOINTS + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION X(TOTALPOINTS, D) + + INTEGER KL, K, I, J, LEV, IDX, NPTS_KL + INTEGER DIM_ACTIVE(50), NDIMS + INTEGER REP(50), NPTS_DIM(50) + INTEGER NX + DOUBLE PRECISION ABSC(127) + + IDX = 1 + + DO K = 1, D + DO I = 1, TOTALPOINTS + X(I, K) = 0.5D+00 + END DO + END DO + + DO KL = 1, NLEVELS + + NDIMS = 0 + NPTS_KL = 1 + DO K = 1, D + LEV = LEVELSEQ(KL, K) + IF (LEV .EQ. 0) THEN + NPTS_DIM(K) = 1 + ELSE + NPTS_DIM(K) = 2**LEV + NDIMS = NDIMS + 1 + DIM_ACTIVE(NDIMS) = K + END IF + NPTS_KL = NPTS_KL * NPTS_DIM(K) + END DO + + REP(1) = 1 + DO K = 2, NDIMS + REP(K) = REP(K-1) * NPTS_DIM(DIM_ACTIVE(K-1)) + END DO + + DO I = 0, NPTS_KL - 1 + DO K = 1, NDIMS + LEV = LEVELSEQ(KL, DIM_ACTIVE(K)) + NX = 2**(LEV+1) - 1 + CALL GP_ABSC(LEV, ABSC, NX) + IF (NDIMS .EQ. 1) THEN + J = MOD(I, NPTS_DIM(DIM_ACTIVE(K))) + ELSE + J = MOD(I/REP(K), + & NPTS_DIM(DIM_ACTIVE(K))) + END IF +C New GP nodes occupy the even indices (0,2,4,...) +C of the full node set. Skip by 2 to get new nodes. + X(IDX+I, DIM_ACTIVE(K)) = ABSC(J*2 + 1) + END DO + END DO + + IDX = IDX + NPTS_KL + + END DO + + RETURN + END diff --git a/src/spgridgpsp.f b/src/spgridgpsp.f new file mode 100644 index 0000000..cd077dd --- /dev/null +++ b/src/spgridgpsp.f @@ -0,0 +1,111 @@ +C ******************************************************************* +C +C SPGRID_GP_SP - GP grid points, sparse index structure. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGRID_GP_SP(INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, NADDR, + & SUBGRIDPOINTS, D, X, TOTALPOINTS, FROMINDEX, TOINDEX) +C ******************************************************************* +C +C SPGRID_GP_SP computes Gauss-Patterson sparse grid points using +C the sparse index structure. For each lev, NX=2^(lev+1)-1, calls +C GP_ABSC and takes every other node (odd 1-based indices). +C +C Parameters: +C +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION X(TOTALPOINTS,D) - grid coordinates +C Input, INTEGER TOTALPOINTS - total points +C Input, INTEGER FROMINDEX - first subgrid (1-based) +C Input, INTEGER TOINDEX - last subgrid (1-based) +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NSUBGRIDS, NADDR, D, TOTALPOINTS, FROMINDEX, TOINDEX + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + DOUBLE PRECISION X(TOTALPOINTS, D) + + INTEGER CI, DID, NDIMS, ADDR, LVAL, NPTS + INTEGER ACTIVEDIMS(50), ACTIVELEV(50) + INTEGER NPTS_DIM(50), REP(50) + INTEGER I, K, J, IDX, NX + DOUBLE PRECISION ABSC(127) + + DO K = 1, D + DO I = 1, TOTALPOINTS + X(I, K) = 0.5D+00 + END DO + END DO + + IDX = 1 + DO CI = 1, FROMINDEX - 1 + IDX = IDX + SUBGRIDPOINTS(CI) + END DO + + DO CI = FROMINDEX, TOINDEX + + NDIMS = INDICESNDIIMS(CI) + ADDR = INDICESADDR(CI) + NPTS = SUBGRIDPOINTS(CI) + + IF (NDIMS .EQ. 0) THEN + IDX = IDX + NPTS + GOTO 200 + END IF + + DO DID = 1, NDIMS + ACTIVEDIMS(DID) = INDICESDIMS(ADDR + DID - 1) + ACTIVELEV(DID) = INDICESLEVS(ADDR + DID - 1) + END DO + + DO DID = 1, NDIMS + LVAL = ACTIVELEV(DID) + NPTS_DIM(DID) = 2**LVAL + END DO + + REP(1) = 1 + DO DID = 2, NDIMS + REP(DID) = REP(DID-1) * NPTS_DIM(DID-1) + END DO + + DO I = 0, NPTS - 1 + DO DID = 1, NDIMS + LVAL = ACTIVELEV(DID) + NX = 2**(LVAL+1) - 1 + CALL GP_ABSC(LVAL, ABSC, NX) + J = MOD(I / REP(DID), NPTS_DIM(DID)) +C New GP nodes occupy odd 1-based positions (1,3,5,...). +C Skip by 2: index = J*2+1 (1-based). + X(IDX+I, ACTIVEDIMS(DID)) = ABSC(J*2 + 1) + END DO + END DO + + IDX = IDX + NPTS + + 200 CONTINUE + END DO + + RETURN + END diff --git a/src/spgridm.f b/src/spgridm.f new file mode 100644 index 0000000..b7c3056 --- /dev/null +++ b/src/spgridm.f @@ -0,0 +1,93 @@ +C ******************************************************************* +C +C SPGRID_M - Maximum-norm sparse grid point coordinates. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGRID_M(LEVELSEQ, NLEVELS, D, X, TOTALPOINTS) +C ******************************************************************* +C +C SPGRID_M computes Maximum-norm sparse grid points on [0,1]^D. +C +C 1-D node formula: +C lev = 0 -> {0, 0.5, 1} (3 nodes) +C lev >= 1 -> {(2i-1)/2^(lev+1), i=1,...,2^lev} +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION X(TOTALPTS,D) - grid coordinates +C Input, INTEGER TOTALPOINTS - total grid points +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D, TOTALPOINTS + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION X(TOTALPOINTS, D) + + INTEGER KL, K, I, J, LEV, IDX, NPTS_KL, NPTS_DIM(50) + INTEGER REP(50) + DOUBLE PRECISION COORD + + IDX = 1 + + DO KL = 1, NLEVELS + + NPTS_KL = 1 + DO K = 1, D + LEV = LEVELSEQ(KL, K) + IF (LEV .EQ. 0) THEN + NPTS_DIM(K) = 3 + ELSE + NPTS_DIM(K) = 2**LEV + END IF + NPTS_KL = NPTS_KL * NPTS_DIM(K) + END DO + + REP(1) = 1 + DO K = 2, D + REP(K) = REP(K-1) * NPTS_DIM(K-1) + END DO + + DO I = 0, NPTS_KL - 1 + DO K = 1, D + LEV = LEVELSEQ(KL, K) + IF (D .EQ. 1) THEN + J = MOD(I, NPTS_DIM(K)) + ELSE + J = MOD(I / REP(K), NPTS_DIM(K)) + END IF + + IF (LEV .EQ. 0) THEN + IF (J .EQ. 0) THEN + COORD = 0.0D+00 + ELSE IF (J .EQ. 1) THEN + COORD = 0.5D+00 + ELSE + COORD = 1.0D+00 + END IF + ELSE + COORD = DBLE(2*J+1) / + & DBLE(2**(LEV+1)) + END IF + X(IDX+I, K) = COORD + END DO + END DO + + IDX = IDX + NPTS_KL + + END DO + + RETURN + END diff --git a/src/spgridnb.f b/src/spgridnb.f new file mode 100644 index 0000000..b3eec80 --- /dev/null +++ b/src/spgridnb.f @@ -0,0 +1,75 @@ +C ******************************************************************* +C +C SPGRID_NB - NoBoundary sparse grid point coordinates. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPGRID_NB(LEVELSEQ, NLEVELS, D, X, TOTALPOINTS) +C ******************************************************************* +C +C SPGRID_NB computes NoBoundary sparse grid points on [0,1]^D. +C All nodes are interior midpoints; no boundary nodes included. +C +C 1-D node formula (all levels): +C lev >= 0 -> {(2i-1)/2^(lev+1), i=1,...,2^lev} +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION X(TOTALPTS,D) - grid coordinates +C Input, INTEGER TOTALPOINTS - total grid points +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D, TOTALPOINTS + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION X(TOTALPOINTS, D) + + INTEGER KL, K, I, J, LEV, IDX, NPTS_KL + INTEGER NPTS_DIM(50), REP(50) + + IDX = 1 + + DO KL = 1, NLEVELS + + NPTS_KL = 1 + DO K = 1, D + NPTS_DIM(K) = 2**LEVELSEQ(KL, K) + NPTS_KL = NPTS_KL * NPTS_DIM(K) + END DO + + REP(1) = 1 + DO K = 2, D + REP(K) = REP(K-1) * NPTS_DIM(K-1) + END DO + + DO I = 0, NPTS_KL - 1 + DO K = 1, D + LEV = LEVELSEQ(KL, K) + IF (D .EQ. 1) THEN + J = MOD(I, NPTS_DIM(K)) + ELSE + J = MOD(I / REP(K), NPTS_DIM(K)) + END IF + X(IDX+I, K) = DBLE(2*J+1) / + & DBLE(2**(LEV+1)) + END DO + END DO + + IDX = IDX + NPTS_KL + + END DO + + RETURN + END diff --git a/src/spinterp.pyf b/src/spinterp.pyf new file mode 100644 index 0000000..b77ce5c --- /dev/null +++ b/src/spinterp.pyf @@ -0,0 +1,745 @@ +python module spinterp + interface + + ! ------------------------------------------------------------------ + ! SPNLEVELS - number of multi-indices summing to N in D dims + ! ------------------------------------------------------------------ + subroutine spnlevels(n, d, nlevels) + integer intent(in) :: n + integer intent(in) :: d + integer intent(out) :: nlevels + end subroutine spnlevels + + ! ------------------------------------------------------------------ + ! SPGETSEQ - enumerate multi-index level sequences + ! ------------------------------------------------------------------ + subroutine spgetseq(n, d, nlevels, seq) + integer intent(in) :: n + integer intent(in) :: d + integer intent(in) :: nlevels + integer intent(out), dimension(nlevels, d) :: seq + end subroutine spgetseq + + ! ------------------------------------------------------------------ + ! SPDIM_CC - total grid points, CC / Chebyshev sparse grid + ! ------------------------------------------------------------------ + subroutine spdim_cc(levelseq, nlevels, d, totalpoints) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in), dimension(nlevels, d) :: levelseq + integer intent(out) :: totalpoints + end subroutine spdim_cc + + ! ------------------------------------------------------------------ + ! SPDIM_M - total grid points, Maximum / NoBoundary sparse grid + ! ------------------------------------------------------------------ + subroutine spdim_m(levelseq, nlevels, d, totalpoints, boundary) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in) :: boundary + integer intent(in), dimension(nlevels, d) :: levelseq + integer intent(out) :: totalpoints + end subroutine spdim_m + + ! ------------------------------------------------------------------ + ! SPGRID_CC - Clenshaw-Curtis sparse grid coordinates + ! ------------------------------------------------------------------ + subroutine spgrid_cc(levelseq, nlevels, d, x, totalpoints) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in) :: totalpoints + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(totalpoints, d) :: x + end subroutine spgrid_cc + + ! ------------------------------------------------------------------ + ! SPGRID_M - Maximum-norm sparse grid coordinates + ! ------------------------------------------------------------------ + subroutine spgrid_m(levelseq, nlevels, d, x, totalpoints) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in) :: totalpoints + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(totalpoints, d) :: x + end subroutine spgrid_m + + ! ------------------------------------------------------------------ + ! SPGRID_NB - NoBoundary sparse grid coordinates + ! ------------------------------------------------------------------ + subroutine spgrid_nb(levelseq, nlevels, d, x, totalpoints) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in) :: totalpoints + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(totalpoints, d) :: x + end subroutine spgrid_nb + + ! ------------------------------------------------------------------ + ! SPGRID_CB - Chebyshev sparse grid coordinates + ! ------------------------------------------------------------------ + subroutine spgrid_cb(levelseq, nlevels, d, x, totalpoints) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in) :: totalpoints + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(totalpoints, d) :: x + end subroutine spgrid_cb + + ! ------------------------------------------------------------------ + ! SPGRID_GP - Gauss-Patterson sparse grid coordinates + ! ------------------------------------------------------------------ + subroutine spgrid_gp(levelseq, nlevels, d, x, totalpoints) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in) :: totalpoints + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(totalpoints, d) :: x + end subroutine spgrid_gp + + ! ------------------------------------------------------------------ + ! SPINTERP_CC - evaluate CC sparse grid interpolant + ! ------------------------------------------------------------------ + subroutine spinterp_cc(d, z, nz, y, ninterp, levelseq, nlevels, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ninterp + integer intent(in) :: nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ninterp) :: ip + end subroutine spinterp_cc + + ! ------------------------------------------------------------------ + ! SPINTERP_M - evaluate Maximum-norm sparse grid interpolant + ! ------------------------------------------------------------------ + subroutine spinterp_m(d, z, nz, y, ninterp, levelseq, nlevels, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ninterp + integer intent(in) :: nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ninterp) :: ip + end subroutine spinterp_m + + ! ------------------------------------------------------------------ + ! SPINTERP_NB - evaluate NoBoundary sparse grid interpolant + ! ------------------------------------------------------------------ + subroutine spinterp_nb(d, z, nz, y, ninterp, levelseq, nlevels, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ninterp + integer intent(in) :: nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ninterp) :: ip + end subroutine spinterp_nb + + ! ------------------------------------------------------------------ + ! SPINTERP_CB - evaluate Chebyshev sparse grid interpolant + ! ------------------------------------------------------------------ + subroutine spinterp_cb(d, z, nz, y, ninterp, levelseq, nlevels, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ninterp + integer intent(in) :: nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ninterp) :: ip + end subroutine spinterp_cb + + ! ------------------------------------------------------------------ + ! SPINTERP_GP - evaluate Gauss-Patterson sparse grid interpolant + ! ------------------------------------------------------------------ + subroutine spinterp_gp(d, z, nz, y, ninterp, levelseq, nlevels, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ninterp + integer intent(in) :: nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ninterp) :: ip + end subroutine spinterp_gp + + ! ------------------------------------------------------------------ + ! SPCMPVALS_CC - compute hierarchical surplus increments, CC + ! ------------------------------------------------------------------ + subroutine spcmpvals_cc(d, z, nz, y, ny, newlevelseq, nnewlevels, levelseq, nlevels, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ny + integer intent(in) :: nnewlevels + integer intent(in) :: nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ny, d) :: y + integer intent(in), dimension(nnewlevels, d) :: newlevelseq + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ny) :: ip + end subroutine spcmpvals_cc + + ! ------------------------------------------------------------------ + ! SPCMPVALS_M - compute hierarchical surplus increments, Maximum + ! ------------------------------------------------------------------ + subroutine spcmpvals_m(d, z, nz, y, ny, newlevelseq, nnewlevels, levelseq, nlevels, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ny + integer intent(in) :: nnewlevels + integer intent(in) :: nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ny, d) :: y + integer intent(in), dimension(nnewlevels, d) :: newlevelseq + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ny) :: ip + end subroutine spcmpvals_m + + ! ------------------------------------------------------------------ + ! SPCMPVALS_NB - compute hierarchical surplus increments, NoBoundary + ! ------------------------------------------------------------------ + subroutine spcmpvals_nb(d, z, nz, y, ny, newlevelseq, nnewlevels, levelseq, nlevels, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ny + integer intent(in) :: nnewlevels + integer intent(in) :: nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ny, d) :: y + integer intent(in), dimension(nnewlevels, d) :: newlevelseq + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ny) :: ip + end subroutine spcmpvals_nb + + ! ------------------------------------------------------------------ + ! SPCMPVALS_CB - compute hierarchical surplus increments, Chebyshev + ! ------------------------------------------------------------------ + subroutine spcmpvals_cb(d, z, nz, y, ny, newlevelseq, nnewlevels, levelseq, nlevels, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ny + integer intent(in) :: nnewlevels + integer intent(in) :: nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ny, d) :: y + integer intent(in), dimension(nnewlevels, d) :: newlevelseq + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ny) :: ip + end subroutine spcmpvals_cb + + ! ------------------------------------------------------------------ + ! SPCMPVALS_GP - compute hierarchical surplus increments, GP + ! ------------------------------------------------------------------ + subroutine spcmpvals_gp(d, z, nz, y, ny, newlevelseq, nnewlevels, levelseq, nlevels, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ny + integer intent(in) :: nnewlevels + integer intent(in) :: nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ny, d) :: y + integer intent(in), dimension(nnewlevels, d) :: newlevelseq + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ny) :: ip + end subroutine spcmpvals_gp + + ! ------------------------------------------------------------------ + ! SPDERIV_CC - CC interpolant values and gradient + ! ------------------------------------------------------------------ + subroutine spderiv_cc(d, z, nz, y, ninterp, levelseq, nlevels, ip, ipder) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ninterp + integer intent(in) :: nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ninterp) :: ip + double precision intent(out), dimension(ninterp, d) :: ipder + end subroutine spderiv_cc + + ! ------------------------------------------------------------------ + ! SPQUADW_CC - quadrature weights, CC / Chebyshev + ! ------------------------------------------------------------------ + subroutine spquadw_cc(levelseq, nlevels, d, w, nw) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in) :: nw + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(nw) :: w + end subroutine spquadw_cc + + ! ------------------------------------------------------------------ + ! SPQUADW_M - quadrature weights, Maximum + ! ------------------------------------------------------------------ + subroutine spquadw_m(levelseq, nlevels, d, w, nw) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in) :: nw + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(nw) :: w + end subroutine spquadw_m + + ! ------------------------------------------------------------------ + ! SPQUADW_NB - quadrature weights, NoBoundary + ! ------------------------------------------------------------------ + subroutine spquadw_nb(levelseq, nlevels, d, w, nw) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in) :: nw + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(nw) :: w + end subroutine spquadw_nb + + ! ------------------------------------------------------------------ + ! SPQUADW_CB - quadrature weights, Chebyshev (needs 1-D table) + ! ------------------------------------------------------------------ + subroutine spquadw_cb(levelseq, nlevels, d, w, nw, w1d, nw1d, startid) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in) :: nw + integer intent(in) :: nw1d + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(nw) :: w + double precision intent(in), dimension(nw1d) :: w1d + integer intent(in), dimension(*) :: startid + end subroutine spquadw_cb + + ! ------------------------------------------------------------------ + ! SPQUADW_GP - quadrature weights, Gauss-Patterson (needs 1-D table) + ! ------------------------------------------------------------------ + subroutine spquadw_gp(levelseq, nlevels, d, w, nw, w1d, nw1d, startid) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in) :: nw + integer intent(in) :: nw1d + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(nw) :: w + double precision intent(in), dimension(nw1d) :: w1d + integer intent(in), dimension(*) :: startid + end subroutine spquadw_gp + + ! ------------------------------------------------------------------ + ! GP_WEIGHTS - 1-D GP quadrature weight table + ! ------------------------------------------------------------------ + subroutine gp_weights(maxlev, weights, nw, startid) + integer intent(in) :: maxlev + integer intent(in) :: nw + double precision intent(out), dimension(nw) :: weights + integer intent(out), dimension(maxlev+1) :: startid + end subroutine gp_weights + + ! ------------------------------------------------------------------ + ! CHEB_WEIGHTS - 1-D Chebyshev quadrature weight table + ! ------------------------------------------------------------------ + subroutine cheb_weights(maxlev, weights, nw, startid) + integer intent(in) :: maxlev + integer intent(in) :: nw + double precision intent(out), dimension(nw) :: weights + integer intent(out), dimension(maxlev+1) :: startid + end subroutine cheb_weights + + ! ------------------------------------------------------------------ + ! GP_ABSC - Gauss-Patterson abscissae + ! ------------------------------------------------------------------ + subroutine gp_absc(level, x, nx) + integer intent(in) :: level + integer intent(in) :: nx + double precision intent(out), dimension(nx) :: x + end subroutine gp_absc + + ! ------------------------------------------------------------------ + ! GP_BARY_W - Gauss-Patterson barycentric weights + ! ------------------------------------------------------------------ + subroutine gp_bary_w(level, w, nw) + integer intent(in) :: level + integer intent(in) :: nw + double precision intent(out), dimension(nw) :: w + end subroutine gp_bary_w + + ! ------------------------------------------------------------------ + ! SPGET_NPOINTS_CC - point counts per subgrid, CC/Chebyshev + ! ------------------------------------------------------------------ + subroutine spget_npoints_cc(levelseq, nlevels, d, totalpoints, npoints) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in), dimension(nlevels, d) :: levelseq + integer intent(out) :: totalpoints + integer intent(out), dimension(nlevels) :: npoints + end subroutine spget_npoints_cc + + ! ------------------------------------------------------------------ + ! SPGET_NPOINTS_M - point counts per subgrid, Maximum + ! ------------------------------------------------------------------ + subroutine spget_npoints_m(levelseq, nlevels, d, totalpoints, npoints) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in), dimension(nlevels, d) :: levelseq + integer intent(out) :: totalpoints + integer intent(out), dimension(nlevels) :: npoints + end subroutine spget_npoints_m + + ! ------------------------------------------------------------------ + ! SPGET_NPOINTS_NB - point counts per subgrid, NoBoundary + ! ------------------------------------------------------------------ + subroutine spget_npoints_nb(levelseq, nlevels, d, totalpoints, npoints) + integer intent(in) :: nlevels + integer intent(in) :: d + integer intent(in), dimension(nlevels, d) :: levelseq + integer intent(out) :: totalpoints + integer intent(out), dimension(nlevels) :: npoints + end subroutine spget_npoints_nb + + ! ------------------------------------------------------------------ + ! SPSEQ2FULL - expand sparse index structure to full levelseq + ! ------------------------------------------------------------------ + subroutine spseq2full(indicesndiims, nsubgrids, indicesdims, indiceslevs, indicesaddr, naddr, d, fullseq) + integer intent(in) :: nsubgrids + integer intent(in) :: naddr + integer intent(in) :: d + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indicesdims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(out), dimension(nsubgrids, d) :: fullseq + end subroutine spseq2full + + ! ------------------------------------------------------------------ + ! REORDER_VALS - reorder surplus array (sort dims by level desc) + ! ------------------------------------------------------------------ + subroutine reorder_vals(z, nz, levelseq, nlevels, d) + integer intent(in) :: nz + integer intent(in) :: nlevels + integer intent(in) :: d + double precision intent(inout), dimension(nz) :: z + integer intent(in), dimension(nlevels, d) :: levelseq + end subroutine reorder_vals + + ! ------------------------------------------------------------------ + ! SPINTERP_CC_SP - CC interpolation, sparse index structure + ! ------------------------------------------------------------------ + subroutine spinterp_cc_sp(d, z, nz, y, ninterp, indicesndiims, nsubgrids, indicesdims, indiceslevs, indicesaddr, naddr, subgridpoints, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ninterp + integer intent(in) :: nsubgrids + integer intent(in) :: naddr + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indicesdims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(nsubgrids) :: subgridpoints + double precision intent(out), dimension(ninterp) :: ip + end subroutine spinterp_cc_sp + + ! ------------------------------------------------------------------ + ! SPINTERP_CB_SP - Chebyshev interpolation, sparse index structure + ! ------------------------------------------------------------------ + subroutine spinterp_cb_sp(d, z, nz, y, ninterp, indicesndiims, nsubgrids, indicesdims, indiceslevs, indicesaddr, naddr, subgridpoints, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ninterp + integer intent(in) :: nsubgrids + integer intent(in) :: naddr + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indicesdims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(nsubgrids) :: subgridpoints + double precision intent(out), dimension(ninterp) :: ip + end subroutine spinterp_cb_sp + + ! ------------------------------------------------------------------ + ! SPINTERP_GP_SP - GP interpolation, sparse index structure + ! ------------------------------------------------------------------ + subroutine spinterp_gp_sp(d, z, nz, y, ninterp, indicesndiims, nsubgrids, indicesdims, indiceslevs, indicesaddr, naddr, subgridpoints, ip) + integer intent(in) :: d + integer intent(in) :: nz + integer intent(in) :: ninterp + integer intent(in) :: nsubgrids + integer intent(in) :: naddr + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indicesdims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(nsubgrids) :: subgridpoints + double precision intent(out), dimension(ninterp) :: ip + end subroutine spinterp_gp_sp + + ! ------------------------------------------------------------------ + ! SPGRID_CC_SP - CC grid points, sparse index structure + ! ------------------------------------------------------------------ + subroutine spgrid_cc_sp(indicesndiims, nsubgrids, indicesdims, indiceslevs, indicesaddr, naddr, subgridpoints, d, x, totalpoints, fromindex, toindex) + integer intent(in) :: nsubgrids + integer intent(in) :: naddr + integer intent(in) :: d + integer intent(in) :: totalpoints + integer intent(in) :: fromindex + integer intent(in) :: toindex + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indicesdims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(nsubgrids) :: subgridpoints + double precision intent(out), dimension(totalpoints, d) :: x + end subroutine spgrid_cc_sp + + ! ------------------------------------------------------------------ + ! SPGRID_CB_SP - Chebyshev grid points, sparse index structure + ! ------------------------------------------------------------------ + subroutine spgrid_cb_sp(indicesndiims, nsubgrids, indicesdims, indiceslevs, indicesaddr, naddr, subgridpoints, d, x, totalpoints, fromindex, toindex) + integer intent(in) :: nsubgrids + integer intent(in) :: naddr + integer intent(in) :: d + integer intent(in) :: totalpoints + integer intent(in) :: fromindex + integer intent(in) :: toindex + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indicesdims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(nsubgrids) :: subgridpoints + double precision intent(out), dimension(totalpoints, d) :: x + end subroutine spgrid_cb_sp + + ! ------------------------------------------------------------------ + ! SPGRID_GP_SP - GP grid points, sparse index structure + ! ------------------------------------------------------------------ + subroutine spgrid_gp_sp(indicesndiims, nsubgrids, indicesdims, indiceslevs, indicesaddr, naddr, subgridpoints, d, x, totalpoints, fromindex, toindex) + integer intent(in) :: nsubgrids + integer intent(in) :: naddr + integer intent(in) :: d + integer intent(in) :: totalpoints + integer intent(in) :: fromindex + integer intent(in) :: toindex + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indicesdims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(nsubgrids) :: subgridpoints + double precision intent(out), dimension(totalpoints, d) :: x + end subroutine spgrid_gp_sp + + ! ------------------------------------------------------------------ + ! SPCMPVALS_CC_SP - CC surpluses, sparse index structure + ! ------------------------------------------------------------------ + subroutine spcmpvals_cc_sp(d, z, nz, y, ny, indicesndiims, nsubgrids, indicesdims, indiceslevs, indicesaddr, naddr, backwardneighbors, forwardneighbors, nfwd, subgridpoints, subgridaddr, fromindex, toindex, ip) + integer intent(in) :: d, nz, ny, nsubgrids, naddr, nfwd + integer intent(in) :: fromindex, toindex + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ny, d) :: y + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indicesdims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(naddr) :: backwardneighbors + integer intent(in), dimension(nfwd) :: forwardneighbors + integer intent(in), dimension(nsubgrids) :: subgridpoints + integer intent(in), dimension(nsubgrids) :: subgridaddr + double precision intent(out), dimension(ny) :: ip + end subroutine spcmpvals_cc_sp + + ! ------------------------------------------------------------------ + ! SPCMPVALS_CBGP_SP - CB/GP surpluses, sparse index structure + ! ------------------------------------------------------------------ + subroutine spcmpvals_cbgp_sp(d, z, nz, y, ny, indicesndiims, nsubgrids, indicesdims, indiceslevs, indicesaddr, naddr, backwardneighbors, forwardneighbors, nfwd, subgridpoints, subgridaddr, fromindex, toindex, isgp, ip) + integer intent(in) :: d, nz, ny, nsubgrids, naddr, nfwd, isgp + integer intent(in) :: fromindex, toindex + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ny, d) :: y + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indicesdims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(naddr) :: backwardneighbors + integer intent(in), dimension(nfwd) :: forwardneighbors + integer intent(in), dimension(nsubgrids) :: subgridpoints + integer intent(in), dimension(nsubgrids) :: subgridaddr + double precision intent(out), dimension(ny) :: ip + end subroutine spcmpvals_cbgp_sp + + ! ------------------------------------------------------------------ + ! SPQUADW_CC_SP - CC quadrature weights, sparse indices + ! ------------------------------------------------------------------ + subroutine spquadw_cc_sp(indicesndiims, nsubgrids, indiceslevs, indicesaddr, naddr, subgridpoints, w, nw) + integer intent(in) :: nsubgrids, naddr, nw + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(nsubgrids) :: subgridpoints + double precision intent(out), dimension(nw) :: w + end subroutine spquadw_cc_sp + + ! ------------------------------------------------------------------ + ! SPQUADW_CB_SP - CB quadrature weights, sparse indices + ! ------------------------------------------------------------------ + subroutine spquadw_cb_sp(indicesndiims, nsubgrids, indiceslevs, indicesaddr, naddr, subgridpoints, w, nw, w1d, nw1d, startid) + integer intent(in) :: nsubgrids, naddr, nw, nw1d + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(nsubgrids) :: subgridpoints + double precision intent(out), dimension(nw) :: w + double precision intent(in), dimension(nw1d) :: w1d + integer intent(in), dimension(*) :: startid + end subroutine spquadw_cb_sp + + ! ------------------------------------------------------------------ + ! SPQUADW_GP_SP - GP quadrature weights, sparse indices + ! ------------------------------------------------------------------ + subroutine spquadw_gp_sp(indicesndiims, nsubgrids, indiceslevs, indicesaddr, naddr, subgridpoints, w, nw, w1d, nw1d, startid) + integer intent(in) :: nsubgrids, naddr, nw, nw1d + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(nsubgrids) :: subgridpoints + double precision intent(out), dimension(nw) :: w + double precision intent(in), dimension(nw1d) :: w1d + integer intent(in), dimension(*) :: startid + end subroutine spquadw_gp_sp + + ! ------------------------------------------------------------------ + ! SPDERIV_CC_SP - CC gradient, sparse index structure + ! ------------------------------------------------------------------ + subroutine spderiv_cc_sp(d, z, nz, y, ninterp, indicesndiims, nsubgrids, indicesdims, indiceslevs, indicesaddr, naddr, subgridpoints, ip, ipder) + integer intent(in) :: d, nz, ninterp, nsubgrids, naddr + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indicesdims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(nsubgrids) :: subgridpoints + double precision intent(out), dimension(ninterp) :: ip + double precision intent(out), dimension(ninterp, d) :: ipder + end subroutine spderiv_cc_sp + + ! ------------------------------------------------------------------ + ! SPCONT_DERIV_CC - continuous derivatives, CC full levelseq + ! ------------------------------------------------------------------ + subroutine spcont_deriv_cc(d, z, nz, y, ninterp, levelseq, nlevels, maxlev, ip, ipder, ipder2) + integer intent(in) :: d, nz, ninterp, nlevels, maxlev + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ninterp) :: ip + double precision intent(out), dimension(ninterp, d) :: ipder + double precision intent(out), dimension(ninterp, d) :: ipder2 + end subroutine spcont_deriv_cc + + ! ------------------------------------------------------------------ + ! SPCONT_DERIV_CC_SP - continuous derivatives, CC sparse indices + ! ------------------------------------------------------------------ + subroutine spcont_deriv_cc_sp(d, z, nz, y, ninterp, indicesndiims, nsubgrids, indicesdims, indiceslevs, indicesaddr, naddr, subgridpoints, maxlevvec, ip, ipder, ipder2) + integer intent(in) :: d, nz, ninterp, nsubgrids, naddr + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indicesdims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(nsubgrids) :: subgridpoints + integer intent(in), dimension(d) :: maxlevvec + double precision intent(out), dimension(ninterp) :: ip + double precision intent(out), dimension(ninterp, d) :: ipder + double precision intent(out), dimension(ninterp, d) :: ipder2 + end subroutine spcont_deriv_cc_sp + + ! ------------------------------------------------------------------ + ! PP_DERIV - post-processing for continuous derivatives + ! ------------------------------------------------------------------ + subroutine pp_deriv(ipder, ipder2, ninterp, d, maxlevvec, y) + integer intent(in) :: ninterp, d + integer intent(in), dimension(d) :: maxlevvec + double precision intent(inout), dimension(ninterp, d) :: ipder + double precision intent(in), dimension(ninterp, d) :: ipder2 + double precision intent(in), dimension(ninterp, d) :: y + end subroutine pp_deriv + + ! ------------------------------------------------------------------ + ! SPDERIV_CB - Chebyshev gradient, full levelseq + ! ------------------------------------------------------------------ + subroutine spderiv_cb(d, z, nz, y, ninterp, levelseq, nlevels, ip, ipder) + integer intent(in) :: d, nz, ninterp, nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ninterp) :: ip + double precision intent(out), dimension(ninterp, d) :: ipder + end subroutine spderiv_cb + + ! ------------------------------------------------------------------ + ! SPDERIV_CB_SP - Chebyshev gradient, sparse indices + ! ------------------------------------------------------------------ + subroutine spderiv_cb_sp(d, z, nz, y, ninterp, indicesndiims, nsubgrids, indicesdims, indiceslevs, indicesaddr, naddr, subgridpoints, ip, ipder) + integer intent(in) :: d, nz, ninterp, nsubgrids, naddr + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ninterp, d) :: y + integer intent(in), dimension(nsubgrids) :: indicesndiims + integer intent(in), dimension(naddr) :: indicesdims + integer intent(in), dimension(naddr) :: indiceslevs + integer intent(in), dimension(nsubgrids) :: indicesaddr + integer intent(in), dimension(nsubgrids) :: subgridpoints + double precision intent(out), dimension(ninterp) :: ip + double precision intent(out), dimension(ninterp, d) :: ipder + end subroutine spderiv_cb_sp + + ! ------------------------------------------------------------------ + ! SPCMPVALS_CB_DCT - CB surpluses via DCT, full levelseq + ! ------------------------------------------------------------------ + subroutine spcmpvals_cb_dct(d, z, nz, y, ny, newlevelseq, nnewlevels, levelseq, nlevels, ip) + integer intent(in) :: d, nz, ny, nnewlevels, nlevels + double precision intent(in), dimension(nz) :: z + double precision intent(in), dimension(ny, d) :: y + integer intent(in), dimension(nnewlevels, d) :: newlevelseq + integer intent(in), dimension(nlevels, d) :: levelseq + double precision intent(out), dimension(ny) :: ip + end subroutine spcmpvals_cb_dct + + ! ------------------------------------------------------------------ + ! SORT_HEAP - max-heap sift-up + ! ------------------------------------------------------------------ + subroutine sort_heap(a, na, g, ng) + integer intent(in) :: na, ng + integer intent(inout), dimension(na) :: a + double precision intent(in), dimension(ng) :: g + end subroutine sort_heap + + ! ------------------------------------------------------------------ + ! POP_HEAP - pop max from heap + ! ------------------------------------------------------------------ + subroutine pop_heap(a, na, g, ng, index) + integer intent(in) :: na, ng + integer intent(inout), dimension(na) :: a + double precision intent(in), dimension(ng) :: g + integer intent(out) :: index + end subroutine pop_heap + + ! ------------------------------------------------------------------ + ! SPGETSEQ_SP - build sparse multi-index structure + ! ------------------------------------------------------------------ + subroutine spgetseq_sp(n, d, gridcode, indicesndiims, indicesdims, indiceslevs, indicesaddr, backwardneighbors, forwardneighbors, subgridpoints, subgridaddr, activeindices, nsubgrids, maxaddr, currentindex) + integer intent(in) :: n, d, gridcode, nsubgrids, maxaddr + integer intent(out), dimension(nsubgrids) :: indicesndiims + integer intent(out), dimension(maxaddr) :: indicesdims + integer intent(out), dimension(maxaddr) :: indiceslevs + integer intent(out), dimension(nsubgrids) :: indicesaddr + integer intent(out), dimension(maxaddr) :: backwardneighbors + integer intent(out), dimension(nsubgrids*d) :: forwardneighbors + integer intent(out), dimension(nsubgrids) :: subgridpoints + integer intent(out), dimension(nsubgrids) :: subgridaddr + integer intent(out), dimension(nsubgrids) :: activeindices + integer intent(out) :: currentindex + end subroutine spgetseq_sp + + end interface +end python module spinterp diff --git a/src/spinterpcb.f b/src/spinterpcb.f new file mode 100644 index 0000000..c1e60b3 --- /dev/null +++ b/src/spinterpcb.f @@ -0,0 +1,130 @@ +C ******************************************************************* +C +C SPINTERP_CB - Polynomial interpolation, Chebyshev grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPINTERP_CB(D, Z, NZ, Y, NINTERP, + & LEVELSEQ, NLEVELS, IP) +C ******************************************************************* +C +C SPINTERP_CB evaluates the Chebyshev sparse grid interpolant at +C NINTERP query points Y using hierarchical surpluses Z. +C +C For each subgrid with >1 node, calls BARY_PD_STEP_CB with +C dimensions sorted descending by level so the largest node count +C is the innermost loop. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points in [0,1]^D +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index set +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NINTERP) + + INTEGER KL, K, L, LVAL, NPTS, NDIMS, INDEX + INTEGER ALLNX(50), DIMS(50), NORD(50), LEVEL(50) + INTEGER ORDERARR(50), XTOT + DOUBLE PRECISION XBUF(16384) + DOUBLE PRECISION IPTEMP(65536) + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + + NPTS = 1 + NDIMS = 0 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + LEVEL(L) = LVAL + IF (LVAL .EQ. 0) THEN + ELSE IF (LVAL .LE. 2) THEN + NPTS = NPTS * 2 + ELSE + NPTS = NPTS * 2**(LVAL-1) + END IF + END DO + + IF (NPTS .EQ. 1) THEN +C Only one (constant) node, no interpolation needed + DO K = 1, NINTERP + IP(K) = IP(K) + Z(INDEX) + END DO + INDEX = INDEX + 1 + + ELSE +C Sort dimensions descending by level + DO L = 1, D + ORDERARR(L) = L + END DO + DO L = 1, D-1 + DO K = L+1, D + IF (LEVEL(ORDERARR(K)) .GT. + & LEVEL(ORDERARR(L))) THEN + LVAL = ORDERARR(L) + ORDERARR(L) = ORDERARR(K) + ORDERARR(K) = LVAL + END IF + END DO + END DO + +C Build ALLNX and DIMS for active (non-zero level) dims + NDIMS = 0 + XTOT = 0 + DO L = 1, D + LVAL = LEVEL(ORDERARR(L)) + IF (LVAL .GT. 0) THEN + NDIMS = NDIMS + 1 + DIMS(NDIMS) = ORDERARR(L) + ALLNX(NDIMS) = 2**LVAL + 1 + XTOT = XTOT + ALLNX(NDIMS) + END IF + END DO + +C Build node array + CALL GET_CHEB_NODES(ALLNX, NDIMS, XBUF, XTOT) + +C Call barycentric step + DO K = 1, NINTERP + IPTEMP(K) = 0.0D+00 + END DO + CALL BARY_PD_STEP_CB(Z(INDEX), NPTS, + & ALLNX, DIMS, NDIMS, + & XBUF, XTOT, Y, NINTERP, D, IPTEMP) + + DO K = 1, NINTERP + IP(K) = IP(K) + IPTEMP(K) + END DO + + INDEX = INDEX + NPTS + END IF + + END DO + + RETURN + END diff --git a/src/spinterpcbsp.f b/src/spinterpcbsp.f new file mode 100644 index 0000000..dc7f3bd --- /dev/null +++ b/src/spinterpcbsp.f @@ -0,0 +1,133 @@ +C ******************************************************************* +C +C SPINTERP_CB_SP - Chebyshev interpolation, sparse index structure. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPINTERP_CB_SP(D, Z, NZ, Y, NINTERP, + & INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, NADDR, + & SUBGRIDPOINTS, IP) +C ******************************************************************* +C +C SPINTERP_CB_SP evaluates the Chebyshev sparse grid interpolant +C using the sparse index structure (packed active dims/levels). +C For each subgrid with active dims, sort by level descending and +C call BARY_PD_STEP_CB. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NSUBGRIDS, NADDR + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + DOUBLE PRECISION IP(NINTERP) + + INTEGER CI, K, DID, NDIMS, ADDR + INTEGER LVAL, NPTS, XTOT + INTEGER ACTIVEDIMS(50), ACTIVELEV(50) + INTEGER ALLNX(50), DIMS(50), ORDER(50) + INTEGER TMP, I, J + DOUBLE PRECISION XBUF(16384) + DOUBLE PRECISION IPTEMP(65536) + INTEGER INDEX + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + END DO + + INDEX = 1 + + DO CI = 1, NSUBGRIDS + + NDIMS = INDICESNDIIMS(CI) + ADDR = INDICESADDR(CI) + NPTS = SUBGRIDPOINTS(CI) + + IF (NDIMS .EQ. 0) THEN + DO K = 1, NINTERP + IP(K) = IP(K) + Z(INDEX) + END DO + INDEX = INDEX + NPTS + GOTO 200 + END IF + +C Read active dims and levels + DO DID = 1, NDIMS + ACTIVEDIMS(DID) = INDICESDIMS(ADDR + DID - 1) + ACTIVELEV(DID) = INDICESLEVS(ADDR + DID - 1) + ORDER(DID) = DID + END DO + +C Sort by level descending (insertion sort) + DO I = 2, NDIMS + TMP = ORDER(I) + J = I - 1 + 10 IF (J .GE. 1 .AND. + & ACTIVELEV(ORDER(J)) .LT. ACTIVELEV(TMP)) THEN + ORDER(J+1) = ORDER(J) + J = J - 1 + GOTO 10 + END IF + ORDER(J+1) = TMP + END DO + +C Build ALLNX and DIMS in sorted order + XTOT = 0 + DO I = 1, NDIMS + DID = ORDER(I) + LVAL = ACTIVELEV(DID) + ALLNX(I) = 2**LVAL + 1 + DIMS(I) = ACTIVEDIMS(DID) + XTOT = XTOT + ALLNX(I) + END DO + + CALL GET_CHEB_NODES(ALLNX, NDIMS, XBUF, XTOT) + + DO K = 1, NINTERP + IPTEMP(K) = 0.0D+00 + END DO + CALL BARY_PD_STEP_CB(Z(INDEX), NPTS, + & ALLNX, DIMS, NDIMS, + & XBUF, XTOT, Y, NINTERP, D, IPTEMP) + + DO K = 1, NINTERP + IP(K) = IP(K) + IPTEMP(K) + END DO + + INDEX = INDEX + NPTS + + 200 CONTINUE + END DO + + RETURN + END diff --git a/src/spinterpcc.f b/src/spinterpcc.f new file mode 100644 index 0000000..581de96 --- /dev/null +++ b/src/spinterpcc.f @@ -0,0 +1,148 @@ +C ******************************************************************* +C +C SPINTERP_CC - Multi-linear sparse grid interpolation, +C Clenshaw-Curtis grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPINTERP_CC(D, Z, NZ, Y, NINTERP, LEVELSEQ, NLEVELS, + & IP) +C ******************************************************************* +C +C SPINTERP_CC evaluates the sparse grid interpolant at NINTERP +C query points Y using hierarchical surpluses Z and the +C multi-index set LEVELSEQ. +C +C The grid is on the unit cube [0,1]^D. If your domain differs, +C normalise Y before calling. +C +C Basis function for dimension k at level LEV: +C LEV=0 : phi = 1 (constant, no dependency on y) +C LEV=1 : hat functions centred at 0 and 1 covering [0,1] +C LEV>=2: hat function of width 2/2^LEV centred at +C (2*xp+1)/2^LEV where xp = floor(y*2^(LEV-1)) +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D)- query points in [0,1]^D +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index set +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NINTERP) + + INTEGER KL, K, L, LVAL, NPTS + INTEGER INDEX, INDEX2(D), INDEX3 + INTEGER REPVEC(D), XP + DOUBLE PRECISION YT, TEMP, SCALE + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + +C Compute npoints and cumulative repvec (column strides). + NPTS = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .EQ. 0) THEN + REPVEC(L) = 1 + ELSE IF (LVAL .LT. 3) THEN + REPVEC(L) = 2 + ELSE + REPVEC(L) = 2**(LVAL-1) + END IF + NPTS = NPTS * REPVEC(L) + IF (L .GT. 1) THEN + REPVEC(L) = REPVEC(L) * REPVEC(L-1) + END IF + END DO + + DO K = 1, NINTERP + TEMP = 1.0D+00 + L = 1 + + 10 IF (L .LE. D) THEN + LVAL = LEVELSEQ(KL, L) + YT = Y(K, L) + +C Clamp to [0,1]. + IF (YT .LT. 0.0D+00) YT = 0.0D+00 + IF (YT .GT. 1.0D+00) YT = 1.0D+00 + + IF (LVAL .EQ. 0) THEN + INDEX2(L) = 0 + + ELSE IF (LVAL .EQ. 1) THEN + IF (YT .EQ. 1.0D+00) THEN + INDEX2(L) = 1 + ELSE + XP = INT(YT * 2.0D+00) + IF (XP .EQ. 0) THEN + TEMP = TEMP * + & 2.0D+00*(0.5D+00 - YT) + ELSE + TEMP = TEMP * + & 2.0D+00*(YT - 0.5D+00) + END IF + INDEX2(L) = XP + END IF + + ELSE + IF (YT .EQ. 1.0D+00) THEN + TEMP = 0.0D+00 + ELSE + SCALE = DBLE(2**LVAL) + XP = INT(YT * SCALE / 2.0D+00) + TEMP = TEMP * (1.0D+00 - SCALE * + & DABS(YT - + & DBLE(XP*2+1)/SCALE)) + INDEX2(L) = XP + END IF + END IF + + IF (TEMP .EQ. 0.0D+00) GOTO 20 + + L = L + 1 + GOTO 10 + END IF + +C Accumulate contribution: IP(K) += TEMP * Z(index3). + INDEX3 = INDEX + INDEX2(1) + DO L = 2, D + INDEX3 = INDEX3 + REPVEC(L-1) * INDEX2(L) + END DO + IP(K) = IP(K) + TEMP * Z(INDEX3) + + 20 CONTINUE + + END DO + + INDEX = INDEX + NPTS + + END DO + + RETURN + END diff --git a/src/spinterpccsp.f b/src/spinterpccsp.f new file mode 100644 index 0000000..bfdd88b --- /dev/null +++ b/src/spinterpccsp.f @@ -0,0 +1,169 @@ +C ******************************************************************* +C +C SPINTERP_CC_SP - Multi-linear interpolation, CC grid, sparse idx. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPINTERP_CC_SP(D, Z, NZ, Y, NINTERP, + & INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, NADDR, + & SUBGRIDPOINTS, IP) +C ******************************************************************* +C +C SPINTERP_CC_SP evaluates the CC sparse grid interpolant using +C the sparse index structure (packed active dimensions/levels). +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NSUBGRIDS, NADDR + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + DOUBLE PRECISION IP(NINTERP) + + INTEGER CI, K, DID, NDIMS, ADDR + INTEGER LVAL, NPTS + INTEGER ACTIVEDIMS(50), ACTIVELEV(50) + INTEGER REPVEC(50), NPTS_DIM(50) + INTEGER INDEX, INDEX2(50), INDEX3 + INTEGER XP + DOUBLE PRECISION YT, TEMP, SCALE + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + END DO + + INDEX = 1 + + DO CI = 1, NSUBGRIDS + + NDIMS = INDICESNDIIMS(CI) + ADDR = INDICESADDR(CI) + NPTS = SUBGRIDPOINTS(CI) + + IF (NDIMS .EQ. 0) THEN +C Constant subgrid: add Z(INDEX) to all points + DO K = 1, NINTERP + IP(K) = IP(K) + Z(INDEX) + END DO + INDEX = INDEX + NPTS + GOTO 200 + END IF + +C Read active dims and levels + DO DID = 1, NDIMS + ACTIVEDIMS(DID) = INDICESDIMS(ADDR + DID - 1) + ACTIVELEV(DID) = INDICESLEVS(ADDR + DID - 1) + END DO + +C Compute npts_dim and repvec for active dims + DO DID = 1, NDIMS + LVAL = ACTIVELEV(DID) + IF (LVAL .EQ. 0) THEN + NPTS_DIM(DID) = 1 + ELSE IF (LVAL .LT. 3) THEN + NPTS_DIM(DID) = 2 + ELSE + NPTS_DIM(DID) = 2**(LVAL-1) + END IF + END DO + REPVEC(1) = 1 + DO DID = 2, NDIMS + REPVEC(DID) = REPVEC(DID-1) * NPTS_DIM(DID-1) + END DO + + DO K = 1, NINTERP + TEMP = 1.0D+00 + DID = 1 + + 10 IF (DID .LE. NDIMS) THEN + LVAL = ACTIVELEV(DID) + YT = Y(K, ACTIVEDIMS(DID)) + + IF (YT .LT. 0.0D+00) YT = 0.0D+00 + IF (YT .GT. 1.0D+00) YT = 1.0D+00 + + IF (LVAL .EQ. 0) THEN + INDEX2(DID) = 0 + + ELSE IF (LVAL .EQ. 1) THEN + IF (YT .EQ. 1.0D+00) THEN + INDEX2(DID) = 1 + ELSE + XP = INT(YT * 2.0D+00) + IF (XP .EQ. 0) THEN + TEMP = TEMP * + & 2.0D+00*(0.5D+00 - YT) + ELSE + TEMP = TEMP * + & 2.0D+00*(YT - 0.5D+00) + END IF + INDEX2(DID) = XP + END IF + + ELSE + IF (YT .EQ. 1.0D+00) THEN + TEMP = 0.0D+00 + ELSE + SCALE = DBLE(2**LVAL) + XP = INT(YT * SCALE / 2.0D+00) + TEMP = TEMP * (1.0D+00 - SCALE * + & DABS(YT - + & DBLE(XP*2+1)/SCALE)) + INDEX2(DID) = XP + END IF + END IF + + IF (TEMP .EQ. 0.0D+00) GOTO 100 + + DID = DID + 1 + GOTO 10 + END IF + +C Accumulate: INDEX3 = INDEX + sum_k(REPVEC(k)*INDEX2(k)) + INDEX3 = INDEX + INDEX2(1) + DO DID = 2, NDIMS + INDEX3 = INDEX3 + REPVEC(DID) * INDEX2(DID) + END DO + IP(K) = IP(K) + TEMP * Z(INDEX3) + + 100 CONTINUE + + END DO + + INDEX = INDEX + NPTS + + 200 CONTINUE + END DO + + RETURN + END diff --git a/src/spinterpgp.f b/src/spinterpgp.f new file mode 100644 index 0000000..939840e --- /dev/null +++ b/src/spinterpgp.f @@ -0,0 +1,99 @@ +C ******************************************************************* +C +C SPINTERP_GP - Polynomial interpolation, Gauss-Patterson grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPINTERP_GP(D, Z, NZ, Y, NINTERP, + & LEVELSEQ, NLEVELS, IP) +C ******************************************************************* +C +C SPINTERP_GP evaluates the Gauss-Patterson sparse grid interpolant +C at NINTERP query points Y using hierarchical surpluses Z. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points in [0,1]^D +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index set +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NINTERP) + + INTEGER KL, K, L, LVAL, NPTS, NDIMS, INDEX + INTEGER ALLNX(50), DIMS(50), XTOT + DOUBLE PRECISION XBUF(16384), WBUF(16384) + DOUBLE PRECISION IPTEMP(65536) + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + + NPTS = 1 + NDIMS = 0 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .GT. 0) THEN + NPTS = NPTS * 2**LVAL + NDIMS = NDIMS + 1 + ALLNX(NDIMS) = 2**(LVAL+1) - 1 + DIMS(NDIMS) = L + END IF + END DO + + IF (NPTS .EQ. 1) THEN + DO K = 1, NINTERP + IP(K) = IP(K) + Z(INDEX) + END DO + INDEX = INDEX + 1 + + ELSE + XTOT = 0 + DO L = 1, NDIMS + XTOT = XTOT + ALLNX(L) + END DO + + CALL GET_GP_NODES(ALLNX, NDIMS, XBUF, XTOT) + CALL GET_GP_BARY_W(ALLNX, NDIMS, WBUF, XTOT) + + DO K = 1, NINTERP + IPTEMP(K) = 0.0D+00 + END DO + CALL BARY_PD_STEP_GP(Z(INDEX), NPTS, + & ALLNX, DIMS, NDIMS, + & XBUF, XTOT, Y, NINTERP, D, WBUF, IPTEMP) + + DO K = 1, NINTERP + IP(K) = IP(K) + IPTEMP(K) + END DO + + INDEX = INDEX + NPTS + END IF + + END DO + + RETURN + END diff --git a/src/spinterpgpsp.f b/src/spinterpgpsp.f new file mode 100644 index 0000000..0a21d18 --- /dev/null +++ b/src/spinterpgpsp.f @@ -0,0 +1,117 @@ +C ******************************************************************* +C +C SPINTERP_GP_SP - GP interpolation, sparse index structure. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPINTERP_GP_SP(D, Z, NZ, Y, NINTERP, + & INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, NADDR, + & SUBGRIDPOINTS, IP) +C ******************************************************************* +C +C SPINTERP_GP_SP evaluates the Gauss-Patterson sparse grid +C interpolant using the sparse index structure. +C For each subgrid, ALLNX(k) = 2^(lev+1)-1, calls BARY_PD_STEP_GP. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NSUBGRIDS, NADDR + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + DOUBLE PRECISION IP(NINTERP) + + INTEGER CI, K, DID, NDIMS, ADDR + INTEGER LVAL, NPTS, XTOT + INTEGER ACTIVEDIMS(50), ACTIVELEV(50) + INTEGER ALLNX(50), DIMS(50) + DOUBLE PRECISION XBUF(16384), WBUF(16384) + DOUBLE PRECISION IPTEMP(65536) + INTEGER INDEX + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + END DO + + INDEX = 1 + + DO CI = 1, NSUBGRIDS + + NDIMS = INDICESNDIIMS(CI) + ADDR = INDICESADDR(CI) + NPTS = SUBGRIDPOINTS(CI) + + IF (NDIMS .EQ. 0) THEN + DO K = 1, NINTERP + IP(K) = IP(K) + Z(INDEX) + END DO + INDEX = INDEX + NPTS + GOTO 200 + END IF + +C Read active dims and levels + DO DID = 1, NDIMS + ACTIVEDIMS(DID) = INDICESDIMS(ADDR + DID - 1) + ACTIVELEV(DID) = INDICESLEVS(ADDR + DID - 1) + END DO + +C Build ALLNX and DIMS + XTOT = 0 + DO DID = 1, NDIMS + LVAL = ACTIVELEV(DID) + ALLNX(DID) = 2**(LVAL+1) - 1 + DIMS(DID) = ACTIVEDIMS(DID) + XTOT = XTOT + ALLNX(DID) + END DO + + CALL GET_GP_NODES(ALLNX, NDIMS, XBUF, XTOT) + CALL GET_GP_BARY_W(ALLNX, NDIMS, WBUF, XTOT) + + DO K = 1, NINTERP + IPTEMP(K) = 0.0D+00 + END DO + CALL BARY_PD_STEP_GP(Z(INDEX), NPTS, + & ALLNX, DIMS, NDIMS, + & XBUF, XTOT, Y, NINTERP, D, WBUF, IPTEMP) + + DO K = 1, NINTERP + IP(K) = IP(K) + IPTEMP(K) + END DO + + INDEX = INDEX + NPTS + + 200 CONTINUE + END DO + + RETURN + END diff --git a/src/spinterpm.f b/src/spinterpm.f new file mode 100644 index 0000000..6c8daf8 --- /dev/null +++ b/src/spinterpm.f @@ -0,0 +1,164 @@ +C ******************************************************************* +C +C SPINTERP_M - Multi-linear interpolation, Maximum-norm grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPINTERP_M(D, Z, NZ, Y, NINTERP, + & LEVELSEQ, NLEVELS, IP) +C ******************************************************************* +C +C SPINTERP_M evaluates the Maximum-norm sparse grid interpolant. +C +C For lev=0 each dimension contributes 3 nodes (at 0, 0.5, 1). +C All 2^nlevelzero combinations of midpoint vs boundary nodes are +C accumulated for each query point by an inner repeat loop. +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points in [0,1]^D +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index set +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NINTERP) + + INTEGER KL, K, L, LVAL, NPTS, NLEVELZERO, NREPEATS + INTEGER INDEX, INDEX2(50), INDEX3, REPVEC(50) + INTEGER REPEAT(50), REPSTEP, XP + DOUBLE PRECISION YT, TEMP, SCALE + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + + NPTS = 1 + NLEVELZERO = 0 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .EQ. 0) THEN + REPVEC(L) = 3 + NLEVELZERO = NLEVELZERO + 1 + ELSE + REPVEC(L) = 2**LVAL + END IF + REPEAT(L) = 0 + NPTS = NPTS * REPVEC(L) + IF (L .GT. 1) REPVEC(L) = REPVEC(L) * REPVEC(L-1) + END DO + + NREPEATS = 2**NLEVELZERO - 1 + + DO K = 1, NINTERP + REPSTEP = 0 + + 30 IF (REPSTEP .LE. NREPEATS) THEN + TEMP = 1.0D+00 + L = 1 + + 10 IF (L .LE. D) THEN + LVAL = LEVELSEQ(KL, L) + YT = Y(K, L) + IF (YT .LT. 0.0D+00) YT = 0.0D+00 + IF (YT .GT. 1.0D+00) YT = 1.0D+00 + + IF (LVAL .EQ. 0) THEN + IF (REPEAT(L) .EQ. 0) THEN + INDEX2(L) = 1 + TEMP = TEMP*(1.0D+00 + & - 2.0D+00 + & *DABS(YT-0.5D+00)) + ELSE + IF (YT .EQ. 1.0D+00) THEN + INDEX2(L) = 2 + ELSE + XP = INT(YT*2.0D+00)*2 + IF (XP .EQ. 0) THEN + TEMP = TEMP * + & 2.0D+00* + & (0.5D+00-YT) + ELSE + TEMP = TEMP * + & 2.0D+00* + & (YT-0.5D+00) + END IF + INDEX2(L) = XP + END IF + END IF + ELSE IF (YT .EQ. 1.0D+00) THEN + TEMP = 0.0D+00 + ELSE + SCALE = DBLE(2**LVAL) + XP = INT(YT * SCALE) + TEMP = TEMP*(1.0D+00 + & - 2.0D+00*SCALE + & *DABS(YT-(DBLE(XP) + & +0.5D+00)/SCALE)) + INDEX2(L) = XP + END IF + + IF (TEMP .EQ. 0.0D+00) GOTO 20 + L = L + 1 + GOTO 10 + END IF + + IF (TEMP .GT. 0.0D+00) THEN + INDEX3 = INDEX + INDEX2(1) + DO L = 2, D + INDEX3 = INDEX3 + & + REPVEC(L-1)*INDEX2(L) + END DO + IP(K) = IP(K) + TEMP * Z(INDEX3) + END IF + + 20 REPSTEP = REPSTEP + 1 + IF (REPSTEP .LE. NREPEATS) THEN + DO L = 1, D + IF (LEVELSEQ(KL,L) .EQ. 0) THEN + REPEAT(L) = REPEAT(L) + 1 + IF (REPEAT(L) .GT. 1) THEN + REPEAT(L) = 0 + ELSE + GOTO 30 + END IF + END IF + END DO + END IF + GOTO 30 + END IF + + DO L = 1, D + REPEAT(L) = 0 + END DO + + END DO + + INDEX = INDEX + NPTS + + END DO + + RETURN + END diff --git a/src/spinterpnb.f b/src/spinterpnb.f new file mode 100644 index 0000000..c0e6726 --- /dev/null +++ b/src/spinterpnb.f @@ -0,0 +1,126 @@ +C ******************************************************************* +C +C SPINTERP_NB - Multi-linear interpolation, NoBoundary grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPINTERP_NB(D, Z, NZ, Y, NINTERP, + & LEVELSEQ, NLEVELS, IP) +C ******************************************************************* +C +C SPINTERP_NB evaluates the NoBoundary sparse grid interpolant at +C NINTERP query points Y using hierarchical surpluses Z. +C +C Basis function for dimension k at level LEV (scale = 2^lev): +C lev = 0 : phi = 1 (constant) +C xp = floor(yt * scale) +C xp = 0 : 1 - 2*scale*(yt - 0.5/scale) (left-ext) +C xp = scale-1 : 1 + 2*scale*(yt - (scale-0.5)/scale) +C otherwise : 1 - 2*scale*|yt - (xp+0.5)/scale| +C +C Parameters: +C +C Input, INTEGER D - dimension +C Input, DOUBLE PRECISION Z(NZ) - hierarchical surpluses +C Input, INTEGER NZ - length of Z +C Input, DOUBLE PRECISION Y(NINTERP,D) - query points in [0,1]^D +C Input, INTEGER NINTERP - number of query points +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index set +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Output, DOUBLE PRECISION IP(NINTERP) - interpolated values +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER D, NZ, NINTERP, NLEVELS + DOUBLE PRECISION Z(NZ) + DOUBLE PRECISION Y(NINTERP, D) + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION IP(NINTERP) + + INTEGER KL, K, L, LVAL, NPTS + INTEGER INDEX, INDEX2(50), INDEX3 + INTEGER REPVEC(50), XP + DOUBLE PRECISION YT, TEMP, SCALE + + DO K = 1, NINTERP + IP(K) = 0.0D+00 + END DO + + INDEX = 1 + + DO KL = 1, NLEVELS + + NPTS = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + REPVEC(L) = 2**LVAL + NPTS = NPTS * REPVEC(L) + IF (L .GT. 1) REPVEC(L) = REPVEC(L) * REPVEC(L-1) + END DO + + DO K = 1, NINTERP + TEMP = 1.0D+00 + L = 1 + + 10 IF (L .LE. D) THEN + LVAL = LEVELSEQ(KL, L) + YT = Y(K, L) + IF (YT .LT. 0.0D+00) YT = 0.0D+00 + IF (YT .GT. 1.0D+00) YT = 1.0D+00 + + IF (LVAL .EQ. 0) THEN + INDEX2(L) = 0 + ELSE + SCALE = DBLE(2**LVAL) + IF (YT .EQ. 1.0D+00) THEN + XP = INT(SCALE) - 1 + ELSE + XP = INT(YT * SCALE) + END IF + IF (XP .EQ. 0) THEN + TEMP = TEMP * (1.0D+00 + & - 2.0D+00*SCALE*(YT + & - 0.5D+00/SCALE)) + ELSE IF (XP .EQ. INT(SCALE)-1) THEN + TEMP = TEMP * (1.0D+00 + & + 2.0D+00*SCALE*(YT + & - (SCALE-0.5D+00)/SCALE)) + ELSE + TEMP = TEMP * (1.0D+00 + & - 2.0D+00*SCALE + & *DABS(YT-(DBLE(XP) + & +0.5D+00)/SCALE)) + END IF + INDEX2(L) = XP + END IF + + IF (TEMP .EQ. 0.0D+00) GOTO 20 + L = L + 1 + GOTO 10 + END IF + + INDEX3 = INDEX + INDEX2(1) + DO L = 2, D + INDEX3 = INDEX3 + REPVEC(L-1)*INDEX2(L) + END DO + IP(K) = IP(K) + TEMP * Z(INDEX3) + + 20 CONTINUE + + END DO + + INDEX = INDEX + NPTS + + END DO + + RETURN + END diff --git a/src/spnlevels.f b/src/spnlevels.f new file mode 100644 index 0000000..3c40e84 --- /dev/null +++ b/src/spnlevels.f @@ -0,0 +1,36 @@ +C ******************************************************************* +C +C SPNLEVELS - Count multi-index tuples summing to N in D dims. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPNLEVELS(N, D, NLEVELS) +C ******************************************************************* +C +C SPNLEVELS computes C(N+D-1, D-1): the number of multi-index +C tuples (l_1,...,l_D) with l_1 + ... + l_D = N, l_i >= 0. +C +C Parameters: +C +C Input, INTEGER N - level +C Input, INTEGER D - dimension +C Output, INTEGER NLEVELS - number of multi-indices +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER N, D, NLEVELS + INTEGER NCHOOSEK + + NLEVELS = NCHOOSEK(N + D - 1, D - 1) + + RETURN + END diff --git a/src/spquadwcb.f b/src/spquadwcb.f new file mode 100644 index 0000000..0106121 --- /dev/null +++ b/src/spquadwcb.f @@ -0,0 +1,96 @@ +C ******************************************************************* +C +C SPQUADW_CB - Quadrature weights, Chebyshev grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPQUADW_CB(LEVELSEQ, NLEVELS, D, W, NW, + & W1D, NW1D, STARTID) +C ******************************************************************* +C +C SPQUADW_CB returns the quadrature weights for the Chebyshev +C sparse grid. Caller provides 1-D weights W1D and start indices +C STARTID computed by CHEB_WEIGHTS. +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION W(NW) - quadrature weights +C Input, INTEGER NW - length of W +C Input, DOUBLE PRECISION W1D(NW1D) - 1-D weight table +C Input, INTEGER NW1D - length of W1D +C Input, INTEGER STARTID(*) - start index per level +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D, NW, NW1D + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION W(NW) + DOUBLE PRECISION W1D(NW1D) + INTEGER STARTID(*) + + INTEGER KL, K, L, LVAL, NPTS, INDEX, J + INTEGER NPTS_DIM(50), REP(50) + INTEGER WIDSTART(50), WIDEND(50), WID(50) + INTEGER WID1 + DOUBLE PRECISION WVAL + + INDEX = 1 + + DO KL = 1, NLEVELS + NPTS = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .EQ. 0) THEN + NPTS_DIM(L) = 1 + ELSE IF (LVAL .LE. 2) THEN + NPTS_DIM(L) = 2 + ELSE + NPTS_DIM(L) = 2**(LVAL-1) + END IF + NPTS = NPTS * NPTS_DIM(L) + WIDSTART(L) = STARTID(LVAL+1) + WID(L) = WIDSTART(L) + WIDEND(L) = WID(L) + NPTS_DIM(L) - 1 + END DO + + WID1 = WID(1) + DO K = INDEX, INDEX + NPTS - 1 + WVAL = W1D(WID1) + DO L = 2, D + WVAL = WVAL * W1D(WID(L)) + END DO + W(K) = WVAL + + IF (WID1 .LT. WIDEND(1)) THEN + WID1 = WID1 + 1 + ELSE + WID1 = WIDSTART(1) + DO L = 2, D + IF (WID(L) .LT. WIDEND(L)) THEN + WID(L) = WID(L) + 1 + GOTO 10 + ELSE + WID(L) = WIDSTART(L) + END IF + END DO + END IF + 10 CONTINUE + END DO + + INDEX = INDEX + NPTS + END DO + + RETURN + END diff --git a/src/spquadwcbsp.f b/src/spquadwcbsp.f new file mode 100644 index 0000000..9198b82 --- /dev/null +++ b/src/spquadwcbsp.f @@ -0,0 +1,135 @@ +C ******************************************************************* +C +C SPQUADW_CB_SP - CB quadrature weights, sparse index structure. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPQUADW_CB_SP(INDICESNDIIMS, NSUBGRIDS, + & INDICESLEVS, INDICESADDR, NADDR, + & SUBGRIDPOINTS, W, NW, W1D, NW1D, STARTID) +C ******************************************************************* +C +C SPQUADW_CB_SP returns the Chebyshev quadrature weights using +C the sparse index structure. Levels are sorted descending so +C innermost loop iterates over the largest dimension. +C +C Parameters: +C +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Output, DOUBLE PRECISION W(NW) - quadrature weights +C Input, INTEGER NW - length of W +C Input, DOUBLE PRECISION W1D(NW1D) - 1-D weight table +C Input, INTEGER NW1D - length of W1D +C Input, INTEGER STARTID(*) - start index per level +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NSUBGRIDS, NADDR, NW, NW1D + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + DOUBLE PRECISION W(NW) + DOUBLE PRECISION W1D(NW1D) + INTEGER STARTID(*) + + INTEGER CI, DID, NDIMS, ADDR, LVAL, NPTS + INTEGER K, INDEX + INTEGER NPTS_DIM(50), WIDSTART(50), WIDEND(50), WID(50), WID1 + INTEGER ACTIVELEV(50), ORDER(50), TMP, I, J + DOUBLE PRECISION WVAL + + INDEX = 1 + + DO CI = 1, NSUBGRIDS + + NDIMS = INDICESNDIIMS(CI) + ADDR = INDICESADDR(CI) + NPTS = SUBGRIDPOINTS(CI) + +C Read active levels + DO DID = 1, NDIMS + ACTIVELEV(DID) = INDICESLEVS(ADDR + DID - 1) + ORDER(DID) = DID + END DO + +C Sort by level descending + DO I = 2, NDIMS + TMP = ORDER(I) + J = I - 1 + 10 IF (J .GE. 1 .AND. + & ACTIVELEV(ORDER(J)) .LT. ACTIVELEV(TMP)) THEN + ORDER(J+1) = ORDER(J) + J = J - 1 + GOTO 10 + END IF + ORDER(J+1) = TMP + END DO + + DO DID = 1, NDIMS + LVAL = ACTIVELEV(ORDER(DID)) + IF (LVAL .EQ. 0) THEN + NPTS_DIM(DID) = 1 + ELSE IF (LVAL .LE. 2) THEN + NPTS_DIM(DID) = 2 + ELSE + NPTS_DIM(DID) = 2**(LVAL-1) + END IF + WIDSTART(DID) = STARTID(LVAL+1) + WID(DID) = WIDSTART(DID) + WIDEND(DID) = WID(DID) + NPTS_DIM(DID) - 1 + END DO + + IF (NDIMS .EQ. 0) THEN + DO K = INDEX, INDEX + NPTS - 1 + W(K) = 1.0D+00 + END DO + INDEX = INDEX + NPTS + GOTO 200 + END IF + + WID1 = WID(1) + DO K = INDEX, INDEX + NPTS - 1 + WVAL = W1D(WID1) + DO DID = 2, NDIMS + WVAL = WVAL * W1D(WID(DID)) + END DO + W(K) = WVAL + + IF (WID1 .LT. WIDEND(1)) THEN + WID1 = WID1 + 1 + ELSE + WID1 = WIDSTART(1) + DO DID = 2, NDIMS + IF (WID(DID) .LT. WIDEND(DID)) THEN + WID(DID) = WID(DID) + 1 + GOTO 100 + ELSE + WID(DID) = WIDSTART(DID) + END IF + END DO + END IF + 100 CONTINUE + END DO + + INDEX = INDEX + NPTS + + 200 CONTINUE + END DO + + RETURN + END diff --git a/src/spquadwcc.f b/src/spquadwcc.f new file mode 100644 index 0000000..f31f1e5 --- /dev/null +++ b/src/spquadwcc.f @@ -0,0 +1,71 @@ +C ******************************************************************* +C +C SPQUADW_CC - Quadrature weights, Clenshaw-Curtis / Chebyshev grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPQUADW_CC(LEVELSEQ, NLEVELS, D, W, NW) +C ******************************************************************* +C +C SPQUADW_CC returns the quadrature weights for the CC / Chebyshev +C sparse grid. +C +C The weight for each node in a subgrid is: +C w = 1 / (product over dims of nw_k) +C where: +C lev=0: nw=1, np=1 +C lev=1,2: nw=4, np=2 +C lev>=3: nw=2^lev, np=2^(lev-1) +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION W(NW) - quadrature weights +C Input, INTEGER NW - length of W +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D, NW + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION W(NW) + + INTEGER KL, K, L, LVAL, NP, INDEX + DOUBLE PRECISION WVAL + + INDEX = 1 + + DO KL = 1, NLEVELS + NP = 1 + WVAL = 1.0D+00 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .EQ. 0) THEN + NP = NP * 1 + WVAL = WVAL * 1.0D+00 + ELSE IF (LVAL .LE. 2) THEN + NP = NP * 2 + WVAL = WVAL * 4.0D+00 + ELSE + NP = NP * 2**(LVAL-1) + WVAL = WVAL * DBLE(2**LVAL) + END IF + END DO + DO K = INDEX, INDEX + NP - 1 + W(K) = 1.0D+00 / WVAL + END DO + INDEX = INDEX + NP + END DO + + RETURN + END diff --git a/src/spquadwccsp.f b/src/spquadwccsp.f new file mode 100644 index 0000000..e373084 --- /dev/null +++ b/src/spquadwccsp.f @@ -0,0 +1,79 @@ +C ******************************************************************* +C +C SPQUADW_CC_SP - CC quadrature weights, sparse index structure. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPQUADW_CC_SP(INDICESNDIIMS, NSUBGRIDS, + & INDICESLEVS, INDICESADDR, NADDR, + & SUBGRIDPOINTS, W, NW) +C ******************************************************************* +C +C SPQUADW_CC_SP returns the CC quadrature weights using the sparse +C index structure. +C +C Weight per node: +C lev = 0,1,2 -> 1/4 contribution per dim +C lev >= 3 -> 1/2^lev contribution per dim +C +C Parameters: +C +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Output, DOUBLE PRECISION W(NW) - quadrature weights +C Input, INTEGER NW - length of W +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NSUBGRIDS, NADDR, NW + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + DOUBLE PRECISION W(NW) + + INTEGER CI, DID, NDIMS, ADDR, LVAL, NPTS + INTEGER K, INDEX + DOUBLE PRECISION WVAL + + INDEX = 1 + + DO CI = 1, NSUBGRIDS + + NDIMS = INDICESNDIIMS(CI) + ADDR = INDICESADDR(CI) + NPTS = SUBGRIDPOINTS(CI) + + WVAL = 1.0D+00 + DO DID = 1, NDIMS + LVAL = INDICESLEVS(ADDR + DID - 1) + IF (LVAL .LT. 3) THEN + WVAL = WVAL * 0.25D+00 + ELSE + WVAL = WVAL / DBLE(2**LVAL) + END IF + END DO + + DO K = INDEX, INDEX + NPTS - 1 + W(K) = WVAL + END DO + + INDEX = INDEX + NPTS + + END DO + + RETURN + END diff --git a/src/spquadwgp.f b/src/spquadwgp.f new file mode 100644 index 0000000..28f080a --- /dev/null +++ b/src/spquadwgp.f @@ -0,0 +1,89 @@ +C ******************************************************************* +C +C SPQUADW_GP - Quadrature weights, Gauss-Patterson grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPQUADW_GP(LEVELSEQ, NLEVELS, D, W, NW, + & W1D, NW1D, STARTID) +C ******************************************************************* +C +C SPQUADW_GP returns the quadrature weights for the Gauss-Patterson +C sparse grid. Caller provides 1-D weights W1D and start indices +C STARTID computed by GP_WEIGHTS. +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION W(NW) - quadrature weights +C Input, INTEGER NW - length of W +C Input, DOUBLE PRECISION W1D(NW1D) - 1-D weight table +C Input, INTEGER NW1D - length of W1D +C Input, INTEGER STARTID(*) - start index per level +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D, NW, NW1D + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION W(NW) + DOUBLE PRECISION W1D(NW1D) + INTEGER STARTID(*) + + INTEGER KL, K, L, LVAL, NPTS, INDEX + INTEGER NPTS_DIM(50), REP(50) + INTEGER WIDSTART(50), WIDEND(50), WID(50), WID1 + DOUBLE PRECISION WVAL + + INDEX = 1 + + DO KL = 1, NLEVELS + NPTS = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + NPTS_DIM(L) = 2**LVAL + NPTS = NPTS * NPTS_DIM(L) + WIDSTART(L) = STARTID(LVAL+1) + WID(L) = WIDSTART(L) + WIDEND(L) = WID(L) + NPTS_DIM(L) - 1 + END DO + + WID1 = WID(1) + DO K = INDEX, INDEX + NPTS - 1 + WVAL = W1D(WID1) + DO L = 2, D + WVAL = WVAL * W1D(WID(L)) + END DO + W(K) = WVAL + + IF (WID1 .LT. WIDEND(1)) THEN + WID1 = WID1 + 1 + ELSE + WID1 = WIDSTART(1) + DO L = 2, D + IF (WID(L) .LT. WIDEND(L)) THEN + WID(L) = WID(L) + 1 + GOTO 10 + ELSE + WID(L) = WIDSTART(L) + END IF + END DO + END IF + 10 CONTINUE + END DO + + INDEX = INDEX + NPTS + END DO + + RETURN + END diff --git a/src/spquadwgpsp.f b/src/spquadwgpsp.f new file mode 100644 index 0000000..ef118f8 --- /dev/null +++ b/src/spquadwgpsp.f @@ -0,0 +1,108 @@ +C ******************************************************************* +C +C SPQUADW_GP_SP - GP quadrature weights, sparse index structure. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPQUADW_GP_SP(INDICESNDIIMS, NSUBGRIDS, + & INDICESLEVS, INDICESADDR, NADDR, + & SUBGRIDPOINTS, W, NW, W1D, NW1D, STARTID) +C ******************************************************************* +C +C SPQUADW_GP_SP returns the GP quadrature weights using the sparse +C index structure. npoints per dim = 2^lev (not 2^(lev-1)). +C +C Parameters: +C +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER SUBGRIDPOINTS(NSUBGRIDS) - points per subgrid +C Output, DOUBLE PRECISION W(NW) - quadrature weights +C Input, INTEGER NW - length of W +C Input, DOUBLE PRECISION W1D(NW1D) - 1-D weight table +C Input, INTEGER NW1D - length of W1D +C Input, INTEGER STARTID(*) - start index per level +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NSUBGRIDS, NADDR, NW, NW1D + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER SUBGRIDPOINTS(NSUBGRIDS) + DOUBLE PRECISION W(NW) + DOUBLE PRECISION W1D(NW1D) + INTEGER STARTID(*) + + INTEGER CI, DID, NDIMS, ADDR, LVAL, NPTS + INTEGER K, INDEX + INTEGER NPTS_DIM(50), WIDSTART(50), WIDEND(50), WID(50), WID1 + DOUBLE PRECISION WVAL + + INDEX = 1 + + DO CI = 1, NSUBGRIDS + + NDIMS = INDICESNDIIMS(CI) + ADDR = INDICESADDR(CI) + NPTS = SUBGRIDPOINTS(CI) + + DO DID = 1, NDIMS + LVAL = INDICESLEVS(ADDR + DID - 1) + NPTS_DIM(DID) = 2**LVAL + WIDSTART(DID) = STARTID(LVAL+1) + WID(DID) = WIDSTART(DID) + WIDEND(DID) = WID(DID) + NPTS_DIM(DID) - 1 + END DO + + IF (NDIMS .EQ. 0) THEN + DO K = INDEX, INDEX + NPTS - 1 + W(K) = 1.0D+00 + END DO + INDEX = INDEX + NPTS + GOTO 200 + END IF + + WID1 = WID(1) + DO K = INDEX, INDEX + NPTS - 1 + WVAL = W1D(WID1) + DO DID = 2, NDIMS + WVAL = WVAL * W1D(WID(DID)) + END DO + W(K) = WVAL + + IF (WID1 .LT. WIDEND(1)) THEN + WID1 = WID1 + 1 + ELSE + WID1 = WIDSTART(1) + DO DID = 2, NDIMS + IF (WID(DID) .LT. WIDEND(DID)) THEN + WID(DID) = WID(DID) + 1 + GOTO 100 + ELSE + WID(DID) = WIDSTART(DID) + END IF + END DO + END IF + 100 CONTINUE + END DO + + INDEX = INDEX + NPTS + + 200 CONTINUE + END DO + + RETURN + END diff --git a/src/spquadwm.f b/src/spquadwm.f new file mode 100644 index 0000000..aa8f6b0 --- /dev/null +++ b/src/spquadwm.f @@ -0,0 +1,89 @@ +C ******************************************************************* +C +C SPQUADW_M - Quadrature weights, Maximum-norm grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPQUADW_M(LEVELSEQ, NLEVELS, D, W, NW) +C ******************************************************************* +C +C SPQUADW_M returns the quadrature weights for the Maximum-norm +C sparse grid. +C +C 1-D weight vectors: +C lev=0: [1/4, 1/2, 1/4] (3 nodes: 0, 0.5, 1) +C lev>=1: 1/2^(lev+1) for all 2^lev nodes +C +C The D-dimensional weight is the product of 1-D weights. +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION W(NW) - quadrature weights +C Input, INTEGER NW - length of W +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D, NW + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION W(NW) + + INTEGER KL, K, L, LVAL, NPTS, INDEX + INTEGER NPTS_DIM(50), REP(50) + DOUBLE PRECISION W1D(3, 50), WVAL + INTEGER J + + INDEX = 1 + + DO KL = 1, NLEVELS + NPTS = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + IF (LVAL .EQ. 0) THEN + NPTS_DIM(L) = 3 + W1D(1, L) = 0.25D+00 + W1D(2, L) = 0.50D+00 + W1D(3, L) = 0.25D+00 + ELSE + NPTS_DIM(L) = 2**LVAL + DO J = 1, NPTS_DIM(L) + W1D(J, L) = 1.0D+00/DBLE(2**(LVAL+1)) + END DO + END IF + NPTS = NPTS * NPTS_DIM(L) + END DO + + REP(1) = 1 + DO L = 2, D + REP(L) = REP(L-1) * NPTS_DIM(L-1) + END DO + + DO K = 0, NPTS-1 + WVAL = 1.0D+00 + DO L = 1, D + IF (D .EQ. 1) THEN + J = MOD(K, NPTS_DIM(L)) + 1 + ELSE + J = MOD(K/REP(L), NPTS_DIM(L)) + 1 + END IF + WVAL = WVAL * W1D(J, L) + END DO + W(INDEX + K) = WVAL + END DO + + INDEX = INDEX + NPTS + END DO + + RETURN + END diff --git a/src/spquadwnb.f b/src/spquadwnb.f new file mode 100644 index 0000000..d974297 --- /dev/null +++ b/src/spquadwnb.f @@ -0,0 +1,92 @@ +C ******************************************************************* +C +C SPQUADW_NB - Quadrature weights, NoBoundary grid. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPQUADW_NB(LEVELSEQ, NLEVELS, D, W, NW) +C ******************************************************************* +C +C SPQUADW_NB returns the quadrature weights for the NoBoundary +C sparse grid. +C +C 1-D weight vectors: +C lev=0: [1] +C lev=1: [1/2, 1/2] +C lev>=2: [1/2^lev, 1/2^(lev+1), ..., 1/2^(lev+1), 1/2^lev] +C (endpoints doubled relative to interior) +C +C Parameters: +C +C Input, INTEGER LEVELSEQ(NLEVELS,D) - multi-index array +C Input, INTEGER NLEVELS - rows in LEVELSEQ +C Input, INTEGER D - dimension +C Output, DOUBLE PRECISION W(NW) - quadrature weights +C Input, INTEGER NW - length of W +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NLEVELS, D, NW + INTEGER LEVELSEQ(NLEVELS, D) + DOUBLE PRECISION W(NW) + + INTEGER KL, K, L, LVAL, NPTS, INDEX + INTEGER NPTS_DIM(50), REP(50) + DOUBLE PRECISION W1D(256, 50), WVAL + INTEGER J, NP + + INDEX = 1 + + DO KL = 1, NLEVELS + NPTS = 1 + DO L = 1, D + LVAL = LEVELSEQ(KL, L) + NP = 2**LVAL + NPTS_DIM(L) = NP + IF (LVAL .EQ. 0) THEN + W1D(1, L) = 1.0D+00 + ELSE IF (LVAL .EQ. 1) THEN + W1D(1, L) = 0.5D+00 + W1D(2, L) = 0.5D+00 + ELSE + W1D(1, L) = 1.0D+00 / DBLE(2**LVAL) + DO J = 2, NP-1 + W1D(J, L) = 1.0D+00/DBLE(2**(LVAL+1)) + END DO + W1D(NP, L) = 1.0D+00 / DBLE(2**LVAL) + END IF + NPTS = NPTS * NP + END DO + + REP(1) = 1 + DO L = 2, D + REP(L) = REP(L-1) * NPTS_DIM(L-1) + END DO + + DO K = 0, NPTS-1 + WVAL = 1.0D+00 + DO L = 1, D + IF (D .EQ. 1) THEN + J = MOD(K, NPTS_DIM(L)) + 1 + ELSE + J = MOD(K/REP(L), NPTS_DIM(L)) + 1 + END IF + WVAL = WVAL * W1D(J, L) + END DO + W(INDEX + K) = WVAL + END DO + + INDEX = INDEX + NPTS + END DO + + RETURN + END diff --git a/src/spseq2full.f b/src/spseq2full.f new file mode 100644 index 0000000..882d792 --- /dev/null +++ b/src/spseq2full.f @@ -0,0 +1,66 @@ +C ******************************************************************* +C +C SPSEQ2FULL - Convert sparse index structure to full dense levelseq. +C +C License: +C Sparse Grid Interpolation Toolbox +C Copyright (c) 2006 W. Andreas Klimke, Universitaet Stuttgart +C Copyright (c) 2007-2008 W. A. Klimke. All Rights Reserved. +C Copyright (c) 2026 eggzec. All Rights Reserved. +C See LICENSE for details. +C +C ******************************************************************* + + SUBROUTINE SPSEQ2FULL(INDICESNDIIMS, NSUBGRIDS, + & INDICESDIMS, INDICESLEVS, INDICESADDR, + & NADDR, D, FULLSEQ) +C ******************************************************************* +C +C SPSEQ2FULL expands a sparse index representation (packed arrays +C of active dimension indices and levels) into a full dense level +C matrix FULLSEQ of size NSUBGRIDS x D. +C +C For each subgrid k, the active dims and their levels are stored +C starting at address INDICESADDR(k) in INDICESDIMS / INDICESLEVS. +C Inactive dimensions default to zero. +C +C Parameters: +C +C Input, INTEGER INDICESNDIIMS(NSUBGRIDS) - ndims per subgrid +C Input, INTEGER NSUBGRIDS - number of subgrids +C Input, INTEGER INDICESDIMS(NADDR) - packed dim indices +C Input, INTEGER INDICESLEVS(NADDR) - packed levels +C Input, INTEGER INDICESADDR(NSUBGRIDS) - start addr per subgrid +C Input, INTEGER NADDR - length of packed arrays +C Input, INTEGER D - full dimension +C Output, INTEGER FULLSEQ(NSUBGRIDS,D) - full level matrix +C +C ******************************************************************* + + IMPLICIT NONE + + INTEGER NSUBGRIDS, NADDR, D + INTEGER INDICESNDIIMS(NSUBGRIDS) + INTEGER INDICESDIMS(NADDR) + INTEGER INDICESLEVS(NADDR) + INTEGER INDICESADDR(NSUBGRIDS) + INTEGER FULLSEQ(NSUBGRIDS, D) + + INTEGER K, DID, ADDR, DIM + + DO K = 1, NSUBGRIDS +C Zero out all dimensions + DO DID = 1, D + FULLSEQ(K, DID) = 0 + END DO + +C Fill in active dimensions + ADDR = INDICESADDR(K) + DO DID = 1, INDICESNDIIMS(K) + DIM = INDICESDIMS(ADDR + DID - 1) + FULLSEQ(K, DIM) = INDICESLEVS(ADDR + DID - 1) + END DO + END DO + + RETURN + END diff --git a/tests/.gitkeep b/tests/__init__.py similarity index 100% rename from tests/.gitkeep rename to tests/__init__.py diff --git a/tests/test_derivatives.py b/tests/test_derivatives.py new file mode 100644 index 0000000..8071290 --- /dev/null +++ b/tests/test_derivatives.py @@ -0,0 +1,253 @@ +"""Tests for derivative routines, DCT surpluses, heap, and pp_deriv.""" + +import numpy as np +import pytest +import spinterp + +from tests.test_spinterpcc import _spvals + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _build_z_seq(f, n, d): + """Return (z_flat, seq_flat) for all levels 0..n.""" + all_seq, all_surp = _spvals(f, n, d) + return np.concatenate(all_surp), np.vstack(all_seq) + + +# --------------------------------------------------------------------------- +# SPDERIV_CC (dense, full levelseq) — already partially tested via grids tests +# --------------------------------------------------------------------------- + + +def test_spderiv_cc_constant_zero_gradient(): + f = lambda x, y: 5.0 + z, seq = _build_z_seq(f, 2, 2) + pts = np.array([[0.3, 0.4], [0.7, 0.2]]) + ip, grad = spinterp.spderiv_cc(z, pts, seq) + assert ip == pytest.approx([5.0, 5.0], abs=1e-10) + assert np.allclose(grad, 0.0, atol=1e-10) + + +def test_spderiv_cc_linear_exact(): + f = lambda x, y: 3 * x - 2 * y + z, seq = _build_z_seq(f, 2, 2) + pts = np.array([[0.25, 0.75], [0.5, 0.5], [0.1, 0.9]]) + ip, grad = spinterp.spderiv_cc(z, pts, seq) + exact_ip = 3 * pts[:, 0] - 2 * pts[:, 1] + assert ip == pytest.approx(exact_ip, abs=1e-9) + assert grad[:, 0] == pytest.approx(np.full(3, 3.0), abs=1e-9) + assert grad[:, 1] == pytest.approx(np.full(3, -2.0), abs=1e-9) + + +def test_spderiv_cc_1d_quadratic_convergence(): + f = lambda x: x**2 + errors = [] + pts = np.linspace(0.05, 0.95, 20).reshape(-1, 1) + exact_grad = 2 * pts[:, 0] + for n in range(1, 5): + z, seq = _build_z_seq(f, n, 1) + _, grad = spinterp.spderiv_cc(z, pts, seq) + errors.append(np.max(np.abs(grad[:, 0] - exact_grad))) + for i in range(len(errors) - 1): + assert errors[i + 1] <= errors[i] + 1e-13 + + +# --------------------------------------------------------------------------- +# SPCONT_DERIV_CC + PP_DERIV +# --------------------------------------------------------------------------- + + +def test_spcont_deriv_cc_constant(): + f = lambda x, y: 4.2 + z, seq = _build_z_seq(f, 3, 2) + pts = np.array([[0.3, 0.4], [0.6, 0.7]]) + maxlev = 3 + ip, ipder, ipder2 = spinterp.spcont_deriv_cc(z, pts, seq, maxlev) + assert ip == pytest.approx([4.2, 4.2], abs=1e-10) + assert np.allclose(ipder, 0.0, atol=1e-10) + assert np.allclose(ipder2, 0.0, atol=1e-10) + + +def test_pp_deriv_no_crash(): + """PP_DERIV should run without error and return modified ipder.""" + pts = np.linspace(0.1, 0.9, 5).reshape(-1, 1) + y = np.asfortranarray(np.hstack([pts, pts])) + ipder = np.asfortranarray(np.ones((5, 2))) + ipder2 = np.asfortranarray(np.ones((5, 2)) * 1.1) + maxlevvec = np.array([3, 3], dtype=np.int32) + spinterp.pp_deriv(ipder, ipder2, maxlevvec, y) + assert np.all(np.isfinite(ipder)) + + +def test_pp_deriv_same_sign_linear_interp(): + """When ipd1 and ipd2 have same sign, pp_deriv linearly interpolates.""" + # Single point, single dimension + y = np.array([[0.3]]) + ipder = np.array([[2.0]]) + ipder2 = np.array([[4.0]]) + maxlevvec = np.array([1], dtype=np.int32) # maxlev=1: ytd1=0.25, step=0.5 + spinterp.pp_deriv(ipder, ipder2, maxlevvec, y) + # Both positive → linear interp: 2 + (4-2)/0.5 * (0.3-0.25) = 2 + 4*0.05 = 2.2 + assert ipder[0, 0] == pytest.approx(2.2, abs=1e-10) + + +# --------------------------------------------------------------------------- +# SPDERIV_CB (Chebyshev derivative) +# --------------------------------------------------------------------------- + + +def test_spderiv_cb_constant_zero_gradient(): + f = lambda x: 7.0 + z, seq = _build_z_seq(f, 2, 1) + pts = np.linspace(0.1, 0.9, 8).reshape(-1, 1) + ip, grad = spinterp.spderiv_cb(z, pts, seq) + assert ip == pytest.approx(np.full(8, 7.0), abs=1e-10) + assert np.allclose(grad, 0.0, atol=1e-10) + + +def test_spderiv_cb_linear_1d(): + f = lambda x: 2 * x - 1 + z, seq = _build_z_seq_cb(f, 2, 1) + pts = np.linspace(0.1, 0.9, 10).reshape(-1, 1) + ip, grad = spinterp.spderiv_cb(z, pts, seq) + exact_ip = 2 * pts[:, 0] - 1 + # Values should be accurate + assert ip == pytest.approx(exact_ip, abs=1e-9) + # Gradient should equal 2 (derivative of 2x-1) + assert grad[:, 0] == pytest.approx(np.full(10, 2.0), abs=1e-7) + + +def _build_z_seq_cb(f, n, d): + """Build CB surpluses (CB grid, CB interpolation).""" + all_seq, all_surp = [], [] + for k in range(n + 1): + nl = spinterp.spnlevels(k, d) + seq_k = spinterp.spgetseq(k, d, nl) + tp_k = spinterp.spdim_cc(seq_k) # same point count as CC + x_k = spinterp.spgrid_cb(seq_k, tp_k) + fvals = np.array([f(*x_k[i, :]) for i in range(tp_k)]) + if k == 0: + surp_k = fvals.copy() + else: + z_prev = np.concatenate(all_surp) + seq_prev = np.vstack(all_seq) + interp = spinterp.spcmpvals_cb(z_prev, x_k, seq_k, seq_prev) + surp_k = fvals - interp + all_seq.append(seq_k) + all_surp.append(surp_k) + return np.concatenate(all_surp), np.vstack(all_seq) + + +def test_spderiv_cb_gradient_convergence(): + f = lambda x: x * x + pts = np.linspace(0.05, 0.95, 20).reshape(-1, 1) + errors = [] + for n in range(2, 6): + z, seq = _build_z_seq_cb(f, n, 1) + _, grad = spinterp.spderiv_cb(z, pts, seq) + errors.append(np.max(np.abs(grad[:, 0] - 2 * pts[:, 0]))) + for i in range(len(errors) - 1): + assert errors[i + 1] <= errors[i] + 1e-12 + + +# --------------------------------------------------------------------------- +# SPCMPVALS_CB_DCT vs SPCMPVALS_CB (should give same surpluses) +# --------------------------------------------------------------------------- + + +def test_spcmpvals_cb_dct_matches_barycentric(): + """DCT and barycentric surplus computation should agree.""" + f = lambda x: x**2 - 0.5 + d, n = 1, 3 + # Build first level surpluses (level 0 only) + nlevels_0 = spinterp.spnlevels(0, d) + seq_0 = spinterp.spgetseq(0, d, nlevels_0) + tp_0 = spinterp.spdim_cc(seq_0) + x_0 = spinterp.spgrid_cc(seq_0, tp_0) + surp_0 = np.array([f(*x_0[i, :]) for i in range(tp_0)]) + + # Compute level 1 surpluses with both methods + nlevels_1 = spinterp.spnlevels(1, d) + seq_1 = spinterp.spgetseq(1, d, nlevels_1) + tp_1 = spinterp.spdim_cc(seq_1) + x_1 = spinterp.spgrid_cc(seq_1, tp_1) + fvals_1 = np.array([f(*x_1[i, :]) for i in range(tp_1)]) + + # Barycentric method + interp_bary = spinterp.spcmpvals_cb(surp_0, x_1, seq_1, seq_0) + surp_1_bary = fvals_1 - interp_bary + + # DCT method + interp_dct = spinterp.spcmpvals_cb_dct(surp_0, x_1, seq_1, seq_0) + surp_1_dct = fvals_1 - interp_dct + + np.testing.assert_allclose( + surp_1_dct, + surp_1_bary, + atol=1e-10, + err_msg="DCT and barycentric surpluses disagree", + ) + + +# --------------------------------------------------------------------------- +# SORT_HEAP and POP_HEAP +# --------------------------------------------------------------------------- + + +def test_sort_heap_basic(): + """After inserting element at end, sort_heap should restore max-heap.""" + g = np.array([10.0, 5.0, 8.0, 3.0, 7.0]) + # Build a valid heap manually (1-indexed): [8,5,7,3,10] → after sort + # We simulate: existing heap [8,5,7,3], add 10 at position 5 + a = np.array([1, 2, 3, 4, 5], dtype=np.int32) # indices into g + # After sorting: a[0] should hold index with max g value + spinterp.sort_heap(a, g) + # Index stored at a[0] should have the maximum g value + best_idx = a[0] - 1 # convert to 0-based + assert g[best_idx] == g.max() + + +def test_pop_heap_returns_max(): + """POP_HEAP should return the index of the maximum element.""" + g = np.array([0.0, 4.0, 9.0, 2.0, 7.0]) # 0-based values + # Build a max-heap: create the heap array manually + # Max heap for g[1..5] (1-based): index 3 has g=9 → should be at top + # Build by repeated sort_heap + a = np.array([1, 2, 3, 4, 5], dtype=np.int32) + # Insert elements one by one + for i in range(1, 6): + spinterp.sort_heap(a[:i], g) + + # Now pop: should give index 3 (0-based g[2]=9 via 1-based a index=3) + idx = spinterp.pop_heap(a, g) + # idx is 1-based into g + assert g[idx - 1] == g.max() + + +def test_sort_then_pop_heap(): + """Sort + pop should extract maximum repeatedly.""" + n = 6 + g = np.array([0.0, 3.0, 8.0, 1.0, 5.0, 4.0, 7.0]) # 0-based, 1..6 used + a = np.arange(1, n + 1, dtype=np.int32) + + # Build heap + for i in range(1, n + 1): + spinterp.sort_heap(a[:i], g) + + # Pop all in order — should give descending g values + vals = [] + na = n + while na > 0: + idx = spinterp.pop_heap(a[:na], g) + vals.append(g[idx - 1]) + na -= 1 + if na > 0: + spinterp.sort_heap(a[:na], g) + + # Should be sorted descending + for i in range(len(vals) - 1): + assert vals[i] >= vals[i + 1] diff --git a/tests/test_grids.py b/tests/test_grids.py new file mode 100644 index 0000000..9cbfca0 --- /dev/null +++ b/tests/test_grids.py @@ -0,0 +1,331 @@ +"""Tests for NoBoundary, Maximum, Chebyshev, and GP grid types.""" + +import numpy as np +import pytest +import spinterp + + +# --------------------------------------------------------------------------- +# Generic helper: build surpluses for a given grid type +# --------------------------------------------------------------------------- + + +def _spvals(f, n, d, gridtype="cc"): + """Build hierarchical surpluses for f:[0,1]^d -> R at level n. + + gridtype: 'cc', 'nb', 'm', 'cb', 'gp' + """ + dim_fns = { + "cc": ( + spinterp.spdim_cc, + spinterp.spgrid_cc, + spinterp.spcmpvals_cc, + spinterp.spinterp_cc, + ), + "nb": ( + lambda s: spinterp.spdim_m(s, boundary=0), + spinterp.spgrid_nb, + spinterp.spcmpvals_nb, + spinterp.spinterp_nb, + ), + "m": ( + lambda s: spinterp.spdim_m(s, boundary=1), + spinterp.spgrid_m, + spinterp.spcmpvals_m, + spinterp.spinterp_m, + ), + "cb": ( + spinterp.spdim_cc, + spinterp.spgrid_cb, + spinterp.spcmpvals_cb, + spinterp.spinterp_cb, + ), + "gp": ( + lambda s: spinterp.spdim_m(s, boundary=0), + spinterp.spgrid_gp, + spinterp.spcmpvals_gp, + spinterp.spinterp_gp, + ), + } + spdim, spgrid, spcmpvals, _ = dim_fns[gridtype] + + all_seq, all_surp = [], [] + + for k in range(n + 1): + nlevels_k = spinterp.spnlevels(k, d) + seq_k = spinterp.spgetseq(k, d, nlevels_k) + tp_k = spdim(seq_k) + x_k = spgrid(seq_k, tp_k) + + fvals = np.array([f(*x_k[i, :]) for i in range(tp_k)]) + + if k == 0: + surp_k = fvals.copy() + else: + z_prev = np.concatenate(all_surp) + seq_prev = np.vstack(all_seq) + interp = spcmpvals(z_prev, x_k, seq_k, seq_prev) + surp_k = fvals - interp + + all_seq.append(seq_k) + all_surp.append(surp_k) + + return all_seq, all_surp + + +def _spinterp(all_seq, all_surp, y, gridtype="cc"): + interp_fns = { + "cc": spinterp.spinterp_cc, + "nb": spinterp.spinterp_nb, + "m": spinterp.spinterp_m, + "cb": spinterp.spinterp_cb, + "gp": spinterp.spinterp_gp, + } + fn = interp_fns[gridtype] + result = np.zeros(y.shape[0]) + for seq_k, surp_k in zip(all_seq, all_surp): + result += fn(surp_k, y, seq_k) + return result + + +# --------------------------------------------------------------------------- +# NoBoundary grid tests +# --------------------------------------------------------------------------- + + +def test_nb_grid_in_unit_cube(): + for d in range(1, 4): + for n in range(4): + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + tp = spinterp.spdim_m(seq, boundary=0) + x = spinterp.spgrid_nb(seq, tp) + assert np.all(x > 0.0) and np.all(x < 1.0), ( + f"NB grid d={d} n={n}: nodes outside (0,1)" + ) + + +def test_nb_grid_d1_n0(): + nlevels = spinterp.spnlevels(0, 1) + seq = spinterp.spgetseq(0, 1, nlevels) + tp = spinterp.spdim_m(seq, boundary=0) + x = spinterp.spgrid_nb(seq, tp) + assert tp == 1 + assert x[0, 0] == pytest.approx(0.5) + + +def test_nb_constant_interpolation(): + f = lambda x: 2.71828 + all_seq, all_surp = _spvals(f, 3, 1, "nb") + y = np.linspace(0.05, 0.95, 20).reshape(-1, 1) + ip = _spinterp(all_seq, all_surp, y, "nb") + assert ip == pytest.approx(np.full(20, 2.71828), abs=1e-10) + + +def test_nb_linear_convergence(): + f = lambda x: x + errors = [] + y = np.linspace(0.05, 0.95, 30).reshape(-1, 1) + for n in range(1, 5): + all_seq, all_surp = _spvals(f, n, 1, "nb") + ip = _spinterp(all_seq, all_surp, y, "nb") + errors.append(np.max(np.abs(ip - y[:, 0]))) + for i in range(len(errors) - 1): + assert errors[i + 1] <= errors[i] + 1e-13 + + +# --------------------------------------------------------------------------- +# Maximum grid tests +# --------------------------------------------------------------------------- + + +def test_m_grid_d1_n0(): + nlevels = spinterp.spnlevels(0, 1) + seq = spinterp.spgetseq(0, 1, nlevels) + tp = spinterp.spdim_m(seq, boundary=1) + x = spinterp.spgrid_m(seq, tp) + assert tp == 3 + coords = sorted(x[:, 0].tolist()) + assert coords == pytest.approx([0.0, 0.5, 1.0]) + + +def test_m_grid_d1_n1(): + nlevels = spinterp.spnlevels(1, 1) + seq = spinterp.spgetseq(1, 1, nlevels) + tp = spinterp.spdim_m(seq, boundary=1) + x = spinterp.spgrid_m(seq, tp) + assert tp == 2 + coords = sorted(x[:, 0].tolist()) + assert coords == pytest.approx([0.25, 0.75]) + + +def test_m_constant_interpolation(): + f = lambda x: 1.618 + all_seq, all_surp = _spvals(f, 3, 1, "m") + y = np.linspace(0.0, 1.0, 20).reshape(-1, 1) + ip = _spinterp(all_seq, all_surp, y, "m") + assert ip == pytest.approx(np.full(20, 1.618), abs=1e-10) + + +def test_m_linear_convergence(): + f = lambda x: x + errors = [] + y = np.linspace(0.0, 1.0, 30).reshape(-1, 1) + for n in range(1, 5): + all_seq, all_surp = _spvals(f, n, 1, "m") + ip = _spinterp(all_seq, all_surp, y, "m") + errors.append(np.max(np.abs(ip - y[:, 0]))) + for i in range(len(errors) - 1): + assert errors[i + 1] <= errors[i] + 1e-13 + + +# --------------------------------------------------------------------------- +# Chebyshev grid tests +# --------------------------------------------------------------------------- + + +def test_cb_grid_same_size_as_cc(): + for d in range(1, 4): + for n in range(4): + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + tp_cc = spinterp.spdim_cc(seq) + tp_cb = spinterp.spdim_cc(seq) + assert tp_cc == tp_cb + + +def test_cb_grid_in_unit_cube(): + for d in range(1, 3): + for n in range(4): + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + tp = spinterp.spdim_cc(seq) + x = spinterp.spgrid_cb(seq, tp) + assert np.all(x >= 0.0) and np.all(x <= 1.0) + + +def test_cb_constant_interpolation(): + f = lambda x: 3.14 + all_seq, all_surp = _spvals(f, 2, 1, "cb") + y = np.linspace(0, 1, 20).reshape(-1, 1) + ip = _spinterp(all_seq, all_surp, y, "cb") + assert ip == pytest.approx(np.full(20, 3.14), abs=1e-10) + + +def test_cb_linear_convergence(): + f = lambda x: x + errors = [] + y = np.linspace(0.05, 0.95, 30).reshape(-1, 1) + for n in range(1, 5): + all_seq, all_surp = _spvals(f, n, 1, "cb") + ip = _spinterp(all_seq, all_surp, y, "cb") + errors.append(np.max(np.abs(ip - y[:, 0]))) + for i in range(len(errors) - 1): + assert errors[i + 1] <= errors[i] + 1e-12 + + +# --------------------------------------------------------------------------- +# GP abscissae and weights +# --------------------------------------------------------------------------- + + +def test_gp_absc_level0(): + x = spinterp.gp_absc(0, 1) + assert x[0] == pytest.approx(0.5) + + +def test_gp_absc_level1(): + x = spinterp.gp_absc(1, 3) + assert sorted(x.tolist()) == pytest.approx( + [ + 0.5 - 0.77459666924148337704 / 2, + 0.5, + 0.5 + 0.77459666924148337704 / 2, + ], + abs=1e-12, + ) + + +def test_gp_absc_symmetric(): + for level in range(1, 7): + nx = 2 ** (level + 1) - 1 + x = spinterp.gp_absc(level, nx) + mid = nx // 2 + assert x[mid] == pytest.approx(0.5, abs=1e-12) + for i in range(mid): + assert x[i] + x[nx - 1 - i] == pytest.approx(1.0, abs=1e-12) + + +def test_gp_grid_in_unit_cube(): + for d in range(1, 3): + for n in range(4): + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + tp = spinterp.spdim_m(seq, boundary=0) + x = spinterp.spgrid_gp(seq, tp) + assert np.all(x >= 0.0) and np.all(x <= 1.0) + + +# --------------------------------------------------------------------------- +# Quadrature weights +# --------------------------------------------------------------------------- + + +def test_quadw_cc_all_positive(): + for d in range(1, 4): + for n in range(5): + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + tp = spinterp.spdim_cc(seq) + w = spinterp.spquadw_cc(seq, tp) + assert len(w) == tp + assert np.all(w > 0) + + +def test_quadw_nb_all_positive(): + for d in range(1, 3): + for n in range(4): + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + tp = spinterp.spdim_m(seq, boundary=0) + w = spinterp.spquadw_nb(seq, tp) + assert np.all(w > 0) + + +def test_quadw_m_all_positive(): + for d in range(1, 3): + for n in range(4): + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + tp = spinterp.spdim_m(seq, boundary=1) + w = spinterp.spquadw_m(seq, tp) + assert np.all(w > 0) + + +# --------------------------------------------------------------------------- +# Derivative +# --------------------------------------------------------------------------- + + +def test_deriv_cc_constant(): + f = lambda x, y: 5.0 + d, n = 2, 2 + all_seq, all_surp = _spvals(f, n, d) + z = np.concatenate(all_surp) + seq = np.vstack(all_seq) + y = np.array([[0.3, 0.7], [0.5, 0.5]]) + ip, ipder = spinterp.spderiv_cc(z, y, seq) + assert ip == pytest.approx([5.0, 5.0], abs=1e-10) + assert np.allclose(ipder, 0.0, atol=1e-10) + + +def test_deriv_cc_linear(): + f = lambda x, y: x + 2 * y + d, n = 2, 2 + all_seq, all_surp = _spvals(f, n, d) + z = np.concatenate(all_surp) + seq = np.vstack(all_seq) + y = np.array([[0.3, 0.4], [0.6, 0.2]]) + ip, ipder = spinterp.spderiv_cc(z, y, seq) + exact = y[:, 0] + 2 * y[:, 1] + assert ip == pytest.approx(exact, abs=1e-10) diff --git a/tests/test_npoints.py b/tests/test_npoints.py new file mode 100644 index 0000000..44276bb --- /dev/null +++ b/tests/test_npoints.py @@ -0,0 +1,183 @@ +"""Tests for SPGET_NPOINTS_*, SPSEQ2FULL, REORDER_VALS.""" + +import math + +import numpy as np +import spinterp + + +# --------------------------------------------------------------------------- +# SPGET_NPOINTS_CC +# --------------------------------------------------------------------------- + + +def test_spget_npoints_cc_matches_spdim(): + """SPGET_NPOINTS_CC total should equal SPDIM_CC.""" + for d in range(1, 4): + for n in range(5): + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + tp_from_spdim = spinterp.spdim_cc(seq) + tp, npoints = spinterp.spget_npoints_cc(seq) + assert tp == tp_from_spdim + assert npoints.sum() == tp + assert len(npoints) == nlevels + + +def test_spget_npoints_cc_per_subgrid(): + # D=1, N=3: seq=[[3]], lev=3>=3 → 2^(3-1)=4 points + seq = spinterp.spgetseq(3, 1, 1) + tp, npoints = spinterp.spget_npoints_cc(seq) + assert tp == 4 + assert npoints[0] == 4 + + +# --------------------------------------------------------------------------- +# SPGET_NPOINTS_M +# --------------------------------------------------------------------------- + + +def test_spget_npoints_m_matches_spdim(): + """SPGET_NPOINTS_M total should equal SPDIM_M(..., boundary=1).""" + for d in range(1, 4): + for n in range(4): + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + tp_spdim = spinterp.spdim_m(seq, boundary=1) + tp, npoints = spinterp.spget_npoints_m(seq) + assert tp == tp_spdim + assert npoints.sum() == tp + + +def test_spget_npoints_m_lev0(): + # D=1, N=0: seq=[[0]], lev=0 → 3 points + seq = spinterp.spgetseq(0, 1, 1) + tp, npoints = spinterp.spget_npoints_m(seq) + assert tp == 3 + assert npoints[0] == 3 + + +# --------------------------------------------------------------------------- +# SPGET_NPOINTS_NB +# --------------------------------------------------------------------------- + + +def test_spget_npoints_nb_matches_spdim(): + """SPGET_NPOINTS_NB total should equal SPDIM_M(..., boundary=0).""" + for d in range(1, 4): + for n in range(4): + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + tp_spdim = spinterp.spdim_m(seq, boundary=0) + tp, npoints = spinterp.spget_npoints_nb(seq) + assert tp == tp_spdim + assert npoints.sum() == tp + + +def test_spget_npoints_nb_lev0(): + # D=1, N=0: seq=[[0]], lev=0 → 2^0=1 point + seq = spinterp.spgetseq(0, 1, 1) + tp, npoints = spinterp.spget_npoints_nb(seq) + assert tp == 1 + assert npoints[0] == 1 + + +# --------------------------------------------------------------------------- +# SPSEQ2FULL (requires SPGETSEQ_SP) +# --------------------------------------------------------------------------- + + +def _build_sparse_idx(n, d, gridcode=0): + """Return the sparse index arrays from SPGETSEQ_SP.""" + nsubgrids = math.comb(n + d, d) + maxaddr = max(d * nsubgrids + d, 10) + return spinterp.spgetseq_sp(n, d, gridcode, nsubgrids, maxaddr) + + +def test_spseq2full_d2_n2(): + """SPSEQ2FULL should reconstruct the same multi-index set as the dense approach.""" + n, d = 2, 2 + nsubgrids = math.comb(n + d, d) + + # Build sparse structure + ( + indicesndiims, + indicesdims, + indiceslevs, + indicesaddr, + backward, + forward, + subgridpoints, + subgridaddr, + active, + currentindex, + ) = _build_sparse_idx(n, d) + + # Convert sparse → full + fullseq = spinterp.spseq2full( + indicesndiims, indicesdims, indiceslevs, indicesaddr, d + ) + assert fullseq.shape == (nsubgrids, d) + assert np.all(fullseq >= 0) + + # Build dense multi-index set (all levels 0..n combined) + dense_rows = [] + for k in range(n + 1): + nl = spinterp.spnlevels(k, d) + seq_k = spinterp.spgetseq(k, d, nl) + for row in seq_k: + dense_rows.append(tuple(row.tolist())) + dense_set = set(dense_rows) + + # Sparse full set + sparse_set = set(tuple(row.tolist()) for row in fullseq) + + assert sparse_set == dense_set, ( + f"Sparse multi-index set {sparse_set} != dense {dense_set}" + ) + + +def test_spseq2full_sum_constraints(): + """Each row of SPSEQ2FULL should sum to at most n.""" + for n in range(1, 4): + for d in range(1, 4): + (indicesndiims, indicesdims, indiceslevs, indicesaddr, *_) = ( + _build_sparse_idx(n, d) + ) + fullseq = spinterp.spseq2full( + indicesndiims, indicesdims, indiceslevs, indicesaddr, d + ) + row_sums = fullseq.sum(axis=1) + assert np.all(row_sums <= n) + assert np.all(row_sums >= 0) + + +# --------------------------------------------------------------------------- +# REORDER_VALS +# --------------------------------------------------------------------------- + + +def test_reorder_vals_noop_for_d1(): + """For D=1 there is only one dimension — reordering should be identity.""" + n, d = 3, 1 + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + tp = spinterp.spdim_cc(seq) + z_orig = np.arange(1.0, tp + 1) + z_out = z_orig.copy() + spinterp.reorder_vals(z_out, seq) + np.testing.assert_array_equal(z_out, z_orig) + + +def test_reorder_vals_preserves_content(): + """REORDER_VALS should preserve the set of values, only reorder.""" + n, d = 2, 2 + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + tp = spinterp.spdim_cc(seq) + rng = np.random.default_rng(0) + z = rng.random(tp) + z_out = z.copy() + spinterp.reorder_vals(z_out, seq) + # Content preserved (sorted values equal) + np.testing.assert_allclose(np.sort(z_out), np.sort(z)) diff --git a/tests/test_sparse_idx.py b/tests/test_sparse_idx.py new file mode 100644 index 0000000..62d06c0 --- /dev/null +++ b/tests/test_sparse_idx.py @@ -0,0 +1,363 @@ +"""Tests for sparse-index (SP) variants: grid, interp, surpluses, weights.""" + +import math + +import numpy as np +import pytest +import spinterp + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _build_sparse_idx(n, d, gridcode=0): + nsubgrids = math.comb(n + d, d) + maxaddr = max(d * nsubgrids + d, 10) + ( + indicesndiims, + indicesdims, + indiceslevs, + indicesaddr, + backward, + forward, + subgridpoints, + subgridaddr, + active, + currentindex, + ) = spinterp.spgetseq_sp(n, d, gridcode, nsubgrids, maxaddr) + return dict( + nsubgrids=nsubgrids, + maxaddr=maxaddr, + d=d, + indicesndiims=indicesndiims, + indicesdims=indicesdims, + indiceslevs=indiceslevs, + indicesaddr=indicesaddr, + backwardneighbors=backward, + forwardneighbors=forward, + subgridpoints=subgridpoints, + subgridaddr=subgridaddr, + activeindices=active, + currentindex=currentindex, + ) + + +def _dense_grid_set(n, d, gridtype="cc"): + """Return set of all grid points for levels 0..n (dense).""" + rows = [] + for k in range(n + 1): + nl = spinterp.spnlevels(k, d) + seq = spinterp.spgetseq(k, d, nl) + tp = spinterp.spdim_cc(seq) + if gridtype == "cc": + x = spinterp.spgrid_cc(seq, tp) + elif gridtype == "cb": + x = spinterp.spgrid_cb(seq, tp) + elif gridtype == "gp": + x = spinterp.spgrid_gp(seq, tp) + rows.extend(tuple(round(v, 14) for v in r) for r in x.tolist()) + return set(rows) + + +# --------------------------------------------------------------------------- +# SPGRID_CC_SP +# --------------------------------------------------------------------------- + + +def test_spgrid_cc_sp_same_points_as_dense(): + """Grid points from SP variant should equal those from dense variant.""" + for n in range(1, 4): + for d in range(1, 3): + idx = _build_sparse_idx(n, d) + tp = int(idx["subgridpoints"].sum()) + x_sp = spinterp.spgrid_cc_sp( + idx["indicesndiims"], + idx["indicesdims"], + idx["indiceslevs"], + idx["indicesaddr"], + idx["subgridpoints"], + d, + tp, + 1, + idx["nsubgrids"], + ) + sp_set = set(tuple(round(v, 14) for v in r) for r in x_sp.tolist()) + dense_set = _dense_grid_set(n, d, "cc") + assert sp_set == dense_set, ( + f"n={n} d={d}: SP set size {len(sp_set)} != dense {len(dense_set)}" + ) + + +def test_spgrid_cc_sp_in_unit_cube(): + idx = _build_sparse_idx(3, 2) + tp = int(idx["subgridpoints"].sum()) + x = spinterp.spgrid_cc_sp( + idx["indicesndiims"], + idx["indicesdims"], + idx["indiceslevs"], + idx["indicesaddr"], + idx["subgridpoints"], + 2, + tp, + 1, + idx["nsubgrids"], + ) + assert np.all(x >= 0.0) and np.all(x <= 1.0) + + +# --------------------------------------------------------------------------- +# SPGRID_CB_SP +# --------------------------------------------------------------------------- + + +def test_spgrid_cb_sp_same_points_as_dense(): + for n in range(1, 4): + for d in range(1, 3): + idx = _build_sparse_idx(n, d, gridcode=0) + tp = int(idx["subgridpoints"].sum()) + x_sp = spinterp.spgrid_cb_sp( + idx["indicesndiims"], + idx["indicesdims"], + idx["indiceslevs"], + idx["indicesaddr"], + idx["subgridpoints"], + d, + tp, + 1, + idx["nsubgrids"], + ) + # Compare as sorted arrays with tolerance + dense_pts = [] + for k in range(n + 1): + nl = spinterp.spnlevels(k, d) + seq = spinterp.spgetseq(k, d, nl) + t = spinterp.spdim_cc(seq) + dense_pts.append(spinterp.spgrid_cb(seq, t)) + dense_all = np.vstack(dense_pts) + assert x_sp.shape[0] == dense_all.shape[0], ( + f"n={n} d={d}: CB point count mismatch" + ) + sp_s = x_sp[np.lexsort(x_sp.T[::-1])] + dn_s = dense_all[np.lexsort(dense_all.T[::-1])] + np.testing.assert_allclose( + sp_s, dn_s, atol=1e-10, err_msg=f"n={n} d={d} CB mismatch" + ) + + +# --------------------------------------------------------------------------- +# SPGRID_GP_SP +# --------------------------------------------------------------------------- + + +def test_spgrid_gp_sp_in_unit_cube(): + for n in range(4): + for d in range(1, 3): + idx = _build_sparse_idx(n, d, gridcode=2) + tp = int(idx["subgridpoints"].sum()) + x = spinterp.spgrid_gp_sp( + idx["indicesndiims"], + idx["indicesdims"], + idx["indiceslevs"], + idx["indicesaddr"], + idx["subgridpoints"], + d, + tp, + 1, + idx["nsubgrids"], + ) + assert np.all(x >= 0.0) and np.all(x <= 1.0) + + +# --------------------------------------------------------------------------- +# SPINTERP_CC_SP: constant function +# For a constant f(x)=C, level-0 surplus=C, all others=0. +# Evaluating with z=[C,0,...,0] should give C everywhere. +# --------------------------------------------------------------------------- + + +def test_spinterp_cc_sp_constant(): + C = 3.14 + for n in range(1, 4): + for d in range(1, 3): + idx = _build_sparse_idx(n, d) + tp = int(idx["subgridpoints"].sum()) + z = np.zeros(tp) + z[0] = C # only level-0 subgrid has non-zero surplus + + y = np.random.default_rng(42).random((15, d)) + ip = spinterp.spinterp_cc_sp( + z, + y, + idx["indicesndiims"], + idx["indicesdims"], + idx["indiceslevs"], + idx["indicesaddr"], + idx["subgridpoints"], + ) + assert ip == pytest.approx(np.full(15, C), abs=1e-10), ( + f"n={n} d={d}: constant function failed" + ) + + +def test_spinterp_cb_sp_constant(): + C = 2.718 + n, d = 2, 2 + idx = _build_sparse_idx(n, d) + tp = int(idx["subgridpoints"].sum()) + z = np.zeros(tp) + z[0] = C + + y = np.random.default_rng(7).random((10, d)) + ip = spinterp.spinterp_cb_sp( + z, + y, + idx["indicesndiims"], + idx["indicesdims"], + idx["indiceslevs"], + idx["indicesaddr"], + idx["subgridpoints"], + ) + assert ip == pytest.approx(np.full(10, C), abs=1e-10) + + +def test_spinterp_gp_sp_constant(): + C = 1.618 + n, d = 2, 2 + idx = _build_sparse_idx(n, d, gridcode=2) + tp = int(idx["subgridpoints"].sum()) + z = np.zeros(tp) + z[0] = C + + y = np.random.default_rng(13).random((10, d)) + ip = spinterp.spinterp_gp_sp( + z, + y, + idx["indicesndiims"], + idx["indicesdims"], + idx["indiceslevs"], + idx["indicesaddr"], + idx["subgridpoints"], + ) + assert ip == pytest.approx(np.full(10, C), abs=1e-10) + + +# --------------------------------------------------------------------------- +# SPQUADW_CC_SP: total weight sanity +# --------------------------------------------------------------------------- + + +def test_spquadw_cc_sp_positive_and_length(): + for n in range(5): + for d in range(1, 3): + idx = _build_sparse_idx(n, d) + tp = int(idx["subgridpoints"].sum()) + w = spinterp.spquadw_cc_sp( + idx["indicesndiims"], + idx["indiceslevs"], + idx["indicesaddr"], + idx["subgridpoints"], + tp, + ) + assert len(w) == tp + assert np.all(w > 0) + + +def test_spquadw_cc_sp_matches_dense_for_level0(): + """For level 0, single subgrid with 1 node, weight should be 1.""" + n, d = 0, 2 + idx = _build_sparse_idx(n, d) + tp = int(idx["subgridpoints"].sum()) + w = spinterp.spquadw_cc_sp( + idx["indicesndiims"], + idx["indiceslevs"], + idx["indicesaddr"], + idx["subgridpoints"], + tp, + ) + assert tp == 1 + assert w[0] == pytest.approx(1.0) + + +# --------------------------------------------------------------------------- +# SPDERIV_CC_SP: gradient of linear function should be constant +# --------------------------------------------------------------------------- + + +def test_spderiv_cc_sp_linear_gradient(): + """For f(x,y)=x+2y, gradient should be (1,2) everywhere.""" + from tests.test_spinterpcc import _spvals + + f = lambda x, y: x + 2 * y + d, n = 2, 2 + all_seq, all_surp = _spvals(f, n, d) + + # Build combined z and levelseq + z_all = np.concatenate(all_surp) + seq_all = np.vstack(all_seq) + + # Points at which to evaluate gradient + pts = np.array([[0.3, 0.4], [0.6, 0.2], [0.5, 0.5]]) + + # Dense derivative + ip_dense, grad_dense = spinterp.spderiv_cc(z_all, pts, seq_all) + + # Sparse derivative: build sparse structure, then convert dense levelseq + # to sparse. For comparison, use the dense SPDERIV_CC result as ground truth. + assert grad_dense[:, 0] == pytest.approx(np.ones(3), abs=1e-9) + assert grad_dense[:, 1] == pytest.approx(2 * np.ones(3), abs=1e-9) + + +# --------------------------------------------------------------------------- +# SPQUADW_CB_SP and SPQUADW_GP_SP: smoke tests (don't crash, positive weights) +# --------------------------------------------------------------------------- + + +def test_spquadw_cb_sp_smoke(): + n, d = 2, 2 + idx = _build_sparse_idx(n, d) + tp = int(idx["subgridpoints"].sum()) + + maxlev = 4 + nw1d = sum( + 2 if l == 0 else (2 if l <= 2 else 2 ** (l - 1)) * 2 + for l in range(maxlev + 1) + ) + nw1d = max(nw1d, 100) + weights, startid = spinterp.cheb_weights(maxlev, nw1d) + + w = spinterp.spquadw_cb_sp( + idx["indicesndiims"], + idx["indiceslevs"], + idx["indicesaddr"], + idx["subgridpoints"], + tp, + weights, + startid, + ) + assert len(w) == tp + assert np.all(w > 0) + + +def test_spquadw_gp_sp_smoke(): + n, d = 2, 2 + idx = _build_sparse_idx(n, d, gridcode=2) + tp = int(idx["subgridpoints"].sum()) + + maxlev = 4 + nw1d = sum(2**l * 2 for l in range(maxlev + 1)) + nw1d = max(nw1d, 100) + weights, startid = spinterp.gp_weights(maxlev, nw1d) + + w = spinterp.spquadw_gp_sp( + idx["indicesndiims"], + idx["indiceslevs"], + idx["indicesaddr"], + idx["subgridpoints"], + tp, + weights, + startid, + ) + assert len(w) == tp + assert np.all(w > 0) diff --git a/tests/test_spgetseq.py b/tests/test_spgetseq.py new file mode 100644 index 0000000..e0ca8e4 --- /dev/null +++ b/tests/test_spgetseq.py @@ -0,0 +1,72 @@ +"""Tests for SPGETSEQ and SPNLEVELS.""" + +import numpy as np +import spinterp + + +def test_spnlevels_d1(): + # C(N+0, 0) = 1 for any N when D=1 + assert spinterp.spnlevels(0, 1) == 1 + assert spinterp.spnlevels(3, 1) == 1 + assert spinterp.spnlevels(5, 1) == 1 + + +def test_spnlevels_d2(): + # C(N+1, 1) = N+1 + for n in range(6): + assert spinterp.spnlevels(n, 2) == n + 1 + + +def test_spnlevels_d3(): + # C(N+2, 2) = (N+1)(N+2)/2 + for n in range(6): + expected = (n + 1) * (n + 2) // 2 + assert spinterp.spnlevels(n, 3) == expected + + +def test_spgetseq_d1(): + # Only one multi-index for D=1: [N] + seq = spinterp.spgetseq(3, 1, 1) + assert seq.shape == (1, 1) + assert seq[0, 0] == 3 + + +def test_spgetseq_d2_n2(): + # D=2, N=2: indices summing to 2 + # Expected: [2,0], [1,1], [0,2] + nlevels = spinterp.spnlevels(2, 2) + assert nlevels == 3 + seq = spinterp.spgetseq(2, 2, nlevels) + assert seq.shape == (3, 2) + row_sums = seq.sum(axis=1) + assert np.all(row_sums == 2) + rows = set(map(tuple, seq.tolist())) + assert (2, 0) in rows + assert (1, 1) in rows + assert (0, 2) in rows + + +def test_spgetseq_d3_n3(): + # D=3, N=3: C(5,2)=10 multi-indices + nlevels = spinterp.spnlevels(3, 3) + assert nlevels == 10 + seq = spinterp.spgetseq(3, 3, nlevels) + assert seq.shape == (10, 3) + row_sums = seq.sum(axis=1) + assert np.all(row_sums == 3) + + +def test_spgetseq_first_row(): + # First row always has full level in dimension 1 + for d in range(1, 5): + for n in range(1, 5): + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + assert seq[0, 0] == n + for l in range(1, d): + assert seq[0, l] == 0 + + +def test_spgetseq_all_nonneg(): + seq = spinterp.spgetseq(4, 4, spinterp.spnlevels(4, 4)) + assert np.all(seq >= 0) diff --git a/tests/test_spgridcc.py b/tests/test_spgridcc.py new file mode 100644 index 0000000..488eb6c --- /dev/null +++ b/tests/test_spgridcc.py @@ -0,0 +1,74 @@ +"""Tests for SPDIM_CC and SPGRID_CC.""" + +import numpy as np +import pytest +import spinterp + + +def _get_grid(n, d): + """Helper: build CC sparse grid at level n, dimension d.""" + nlevels = spinterp.spnlevels(n, d) + seq = spinterp.spgetseq(n, d, nlevels) + totalpoints = spinterp.spdim_cc(seq) + x = spinterp.spgrid_cc(seq, totalpoints) + return x, seq, nlevels, totalpoints + + +def test_spdim_cc_d1_n0(): + # D=1, N=0: one subgrid [0], one midpoint 0.5 + nlevels = spinterp.spnlevels(0, 1) + seq = spinterp.spgetseq(0, 1, nlevels) + tp = spinterp.spdim_cc(seq) + assert tp == 1 + + +def test_spdim_cc_d1_n1(): + # D=1, N=1: subgrid [1] -> 2 boundary points + nlevels = spinterp.spnlevels(1, 1) + seq = spinterp.spgetseq(1, 1, nlevels) + tp = spinterp.spdim_cc(seq) + assert tp == 2 + + +def test_spdim_cc_d1_n2(): + # D=1, N=2: subgrid [2] -> 2 interior points + nlevels = spinterp.spnlevels(2, 1) + seq = spinterp.spgetseq(2, 1, nlevels) + tp = spinterp.spdim_cc(seq) + assert tp == 2 + + +def test_grid_in_unit_cube(): + for d in range(1, 4): + for n in range(4): + x, seq, nlevels, tp = _get_grid(n, d) + assert x.shape == (tp, d) + assert np.all(x >= 0.0) + assert np.all(x <= 1.0) + + +def test_grid_d1_n0_coords(): + # Level 0, dim 1: single midpoint at 0.5 + x, *_ = _get_grid(0, 1) + assert x.shape == (1, 1) + assert x[0, 0] == pytest.approx(0.5) + + +def test_grid_d1_n1_coords(): + # Level 1, dim 1: boundary nodes at 0 and 1 + x, *_ = _get_grid(1, 1) + coords = sorted(x[:, 0].tolist()) + assert coords == pytest.approx([0.0, 1.0]) + + +def test_grid_d1_n2_coords(): + # Level 2, dim 1: interior nodes at 0.25 and 0.75 + x, *_ = _get_grid(2, 1) + coords = sorted(x[:, 0].tolist()) + assert coords == pytest.approx([0.25, 0.75]) + + +def test_grid_no_duplicates_d2_n2(): + x, *_ = _get_grid(2, 2) + rows = [tuple(r) for r in x.tolist()] + assert len(rows) == len(set(rows)), "duplicate grid points found" diff --git a/tests/test_spinterpcc.py b/tests/test_spinterpcc.py new file mode 100644 index 0000000..4441757 --- /dev/null +++ b/tests/test_spinterpcc.py @@ -0,0 +1,120 @@ +"""Tests for SPINTERP_CC and SPCMPVALS_CC (full interpolation workflow).""" + +import numpy as np +import pytest +import spinterp + + +def _spvals(f, n, d): + """Compute hierarchical surpluses for f:[0,1]^d -> R at level n. + + Returns (list_of_levelseqs, list_of_surplus_arrays), one per level 0..n. + """ + all_seq = [] + all_surpluses = [] + + for k in range(n + 1): + nlevels_k = spinterp.spnlevels(k, d) + seq_k = spinterp.spgetseq(k, d, nlevels_k) # shape (nlevels_k, d) + tp_k = spinterp.spdim_cc(seq_k) + x_k = spinterp.spgrid_cc(seq_k, tp_k) # shape (tp_k, d) + + fvals = np.array([f(*x_k[i, :]) for i in range(tp_k)]) + + if k == 0: + surpluses_k = fvals.copy() + else: + z_prev = np.concatenate(all_surpluses) + # Combine all previous level sequences into one array + seq_prev = np.vstack(all_seq) + + # Interpolant at new grid points using previous-level surpluses + interp_at_new = spinterp.spcmpvals_cc(z_prev, x_k, seq_k, seq_prev) + surpluses_k = fvals - interp_at_new + + all_seq.append(seq_k) + all_surpluses.append(surpluses_k) + + return all_seq, all_surpluses + + +def _spinterp(all_seq, all_surpluses, y): + """Evaluate the assembled sparse grid interpolant at query points y. + + y: ndarray of shape (npoints, d). + """ + ninterp = y.shape[0] + result = np.zeros(ninterp) + for seq_k, surp_k in zip(all_seq, all_surpluses): + result += spinterp.spinterp_cc(surp_k, y, seq_k) + return result + + +# ------------------------------------------------------------------ +# Test: linear function f(x) = x in 1-D +# A level-1 CC grid reproduces linear functions exactly. +# ------------------------------------------------------------------ +def test_linear_1d_exact(): + f = lambda x: x + d, n = 1, 1 + all_seq, all_surp = _spvals(f, n, d) + y = np.array([[0.0], [0.25], [0.5], [0.75], [1.0]]) + ip = _spinterp(all_seq, all_surp, y) + assert ip == pytest.approx(y[:, 0], abs=1e-12) + + +# ------------------------------------------------------------------ +# Test: constant function f(x) = 3.14 +# ------------------------------------------------------------------ +def test_constant_1d(): + f = lambda x: 3.14 + d, n = 1, 2 + all_seq, all_surp = _spvals(f, n, d) + y = np.linspace(0, 1, 20).reshape(-1, 1) + ip = _spinterp(all_seq, all_surp, y) + assert ip == pytest.approx(np.full(20, 3.14), abs=1e-12) + + +# ------------------------------------------------------------------ +# Test: quadratic f(x) = x^2 error decreases with level in 1-D +# ------------------------------------------------------------------ +def test_quadratic_1d_convergence(): + f = lambda x: x * x + y = np.linspace(0.05, 0.95, 30).reshape(-1, 1) + errors = [] + for n in range(1, 5): + all_seq, all_surp = _spvals(f, n, 1) + ip = _spinterp(all_seq, all_surp, y) + errors.append(np.max(np.abs(ip - y[:, 0] ** 2))) + for i in range(len(errors) - 1): + assert errors[i + 1] <= errors[i] + 1e-14 + + +# ------------------------------------------------------------------ +# Test: 2-D linear f(x,y) = x + y at level 2 +# ------------------------------------------------------------------ +def test_bilinear_2d(): + f = lambda x, y: x + y + d, n = 2, 2 + all_seq, all_surp = _spvals(f, n, d) + rng = np.random.default_rng(42) + pts = rng.random((20, 2)) + ip = _spinterp(all_seq, all_surp, pts) + assert ip == pytest.approx(pts[:, 0] + pts[:, 1], abs=1e-10) + + +# ------------------------------------------------------------------ +# Test: interpolant matches function exactly at the grid nodes +# ------------------------------------------------------------------ +def test_interpolant_at_nodes(): + f = lambda x, y: x**2 + y**2 - 2 * x * y + d, n = 2, 3 + all_seq, all_surp = _spvals(f, n, d) + + pts = np.vstack([ + spinterp.spgrid_cc(seq_k, spinterp.spdim_cc(seq_k)) for seq_k in all_seq + ]) + + ip = _spinterp(all_seq, all_surp, pts) + exact = np.array([f(pts[i, 0], pts[i, 1]) for i in range(len(pts))]) + assert ip == pytest.approx(exact, abs=1e-10) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a4c662e --- /dev/null +++ b/uv.lock @@ -0,0 +1,752 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "click" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/9d/7c83ef51c3eb495f10010094e661833588b7709946da634c8b66520b97c7/coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075", size = 219668, upload-time = "2026-05-10T17:59:23.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/34/898546aefbd28f0af131201d0dc852c9e976f817bd7d5bfb8dc4e02863bb/coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82", size = 220192, upload-time = "2026-05-10T17:59:26.095Z" }, + { url = "https://files.pythonhosted.org/packages/df/4a/b457c88aca72b0df13a98167ebd5d947135ccd9881ea88ce6a570e13aa9b/coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c", size = 246932, upload-time = "2026-05-10T17:59:27.806Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d9/92600e89486fd074c50f0117422b2c9592c3e144e2f25bd5ac0bc62bc7a0/coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893", size = 248762, upload-time = "2026-05-10T17:59:29.479Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e1/9ea1eb9c311da7f15853559dc1d9d82bef88ecd3e59fbeb51f16bc2ffa91/coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20", size = 250625, upload-time = "2026-05-10T17:59:31.33Z" }, + { url = "https://files.pythonhosted.org/packages/a5/03/57afca1b8106f8549a5329139315041fe166d6099bd9381346b9430dfbd1/coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec", size = 252539, upload-time = "2026-05-10T17:59:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/2e9fc63c9928119c1dbae02222be51407d3e7ebac5811ebbda4af3557795/coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757", size = 247636, upload-time = "2026-05-10T17:59:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e2/0b7898cda21041cc67546e19b80ba66cbbb47cbece52a76a5904de6a3aaf/coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a", size = 248666, upload-time = "2026-05-10T17:59:36.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/d33662a2fdaef23229c15921f39c84ec38441f3069ba26e134ed402c833b/coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea", size = 246670, upload-time = "2026-05-10T17:59:38.029Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/533942c3bfbf6770b5c32d7f2ff029fe013dba31f3fe8b45cabbb250365e/coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb", size = 250484, upload-time = "2026-05-10T17:59:39.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/00/15acbad83a96de13c73831486c7627bfed73dfaec53b04e4a6315edf3fd8/coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218", size = 246942, upload-time = "2026-05-10T17:59:41.659Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/cef0228de493f2c740c760a9057a61d00c6849480073b70a75b87c7d4bab/coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85", size = 247544, upload-time = "2026-05-10T17:59:43.471Z" }, + { url = "https://files.pythonhosted.org/packages/77/a0/d9ef8e148f3025c2ae8401d77cda1502b6d2a4d8102603a8af31460aedb6/coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323", size = 222285, upload-time = "2026-05-10T17:59:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/85/c0/30c454c7d3cf47b2805d4e06f12443f5eece8a5d030d3b0350e7b74ecb49/coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a", size = 223215, upload-time = "2026-05-10T17:59:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633, upload-time = "2026-05-10T17:59:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743, upload-time = "2026-05-10T17:59:55.021Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700, upload-time = "2026-05-10T17:59:56.511Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854, upload-time = "2026-05-10T17:59:57.94Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433, upload-time = "2026-05-10T17:59:59.688Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494, upload-time = "2026-05-10T18:00:01.953Z" }, + { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261, upload-time = "2026-05-10T18:00:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216, upload-time = "2026-05-10T18:00:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125, upload-time = "2026-05-10T18:00:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300, upload-time = "2026-05-10T18:00:08.608Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241, upload-time = "2026-05-10T18:00:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908, upload-time = "2026-05-10T18:00:12.242Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, + { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, + { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, + { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, + { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, + { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "deepmerge" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/50/8e/b8041bc719f056afd864478029d52214789341ac6583437b0ee5031e9530/numpy-2.4.5.tar.gz", hash = "sha256:ca670567a5683b7c1670ec03e0ddd5862e10934e92a70751d68d7b7b74ca7f9f", size = 20735669, upload-time = "2026-05-15T20:25:19.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/44/1383ee4d1e916a9e610e46c876b5c83ea023526117d23cd911983929ec34/numpy-2.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3176dc8ff71dbb593606f91a69ad0c3cd3303c7eb546af477370ab9edf760288", size = 16969261, upload-time = "2026-05-15T20:22:23.036Z" }, + { url = "https://files.pythonhosted.org/packages/3d/61/54bacfbec7550bc398e6b6d9a861db35d64f75844e1d7920f5722c3cd5e7/numpy-2.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1811150e5148f5a01a7cc282cb2f489b4a3050a773e173adb480e507bad3a3d7", size = 14964009, upload-time = "2026-05-15T20:22:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/7a/55/fe86c64561761f185339c26001164a2687bd4787af681e961431abd2d534/numpy-2.4.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0d63a780070871210853ba01e90b88f9b85cf2abf63a7f143d5127189265ddf6", size = 5469106, upload-time = "2026-05-15T20:22:28.13Z" }, + { url = "https://files.pythonhosted.org/packages/2f/74/cf29b8317627f0e3aa2c9fb332d386bd734308cecd9e07da9f407d9ce0c3/numpy-2.4.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:0c6919cefafb3b76cd46a89dbb203bf1dd95529d2a6d09fef2d325d95d6a79d8", size = 6798945, upload-time = "2026-05-15T20:22:30.061Z" }, + { url = "https://files.pythonhosted.org/packages/80/a9/b61730a17fa87d5abb13ce560a1b4ce3485d37a13e03eb7b414e598e72f8/numpy-2.4.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d51efede1e58e8b11877536a5518f60e318d8ff69b89ad7b38ee5e431b24d772", size = 15967025, upload-time = "2026-05-15T20:22:32.328Z" }, + { url = "https://files.pythonhosted.org/packages/03/39/70bcd187eb4d223c21fde02c2bdfbffbffef3288cbb3947c04c74ae39a08/numpy-2.4.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07ce7e74da92d7c71b5df157b9758bcdd53d7fea10602154de3afd2b3ddc34dd", size = 16918685, upload-time = "2026-05-15T20:22:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/400fd1315bbe228af3937cf8a74e32023df6217af36077919d00adc382e4/numpy-2.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d7828234a13185effb34979e146f9921f2a65dfbbe215e6dbb57d6478fc8e059", size = 17322963, upload-time = "2026-05-15T20:22:37.557Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/bbbafb657e6f6ee826b4ecdb8722a2e0aae4a981888eaf59eae6a535cc13/numpy-2.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f96083adc3dfc1bbf778f2c79654d88115fa07074c97cb724fe9508f12d91c55", size = 18651594, upload-time = "2026-05-15T20:22:40.449Z" }, + { url = "https://files.pythonhosted.org/packages/de/0c/857a515154a2a18b0dfae04089600d166d352d473ec17a0680d879582d06/numpy-2.4.5-cp311-cp311-win32.whl", hash = "sha256:4ed78c904a638b6e5d7cd4db90c06fca5fc6ec2f28d258305368f454a50e79cf", size = 6233849, upload-time = "2026-05-15T20:22:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/f0/66/d215f3fb93541617adb5d58b3b9508e8a6413e499711e0adc0b80bcb445d/numpy-2.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:079b0fad6f2899b23c5da89792b5409d2d83fc83e8bd5c2299cc9c397a264864", size = 12608238, upload-time = "2026-05-15T20:22:45.229Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c4/611d66d3fcfa931954d37a19ce5575f3283d023e89ff0df6ad43b334ae9c/numpy-2.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:d6c78e260b53affe9b395a9d54fc61f101f9521c4d9452c7e9e3718b19e2215b", size = 10479452, upload-time = "2026-05-15T20:22:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/3275231e98620002681c922e792db04d72c356e9d8073c387344fc0e4ff1/numpy-2.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:654fb8674b61b1c4bd568f944d13a908566fdcb0d797303521d4149d16da05ef", size = 16689166, upload-time = "2026-05-15T20:22:50.761Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/000aab6a16bdec53307f0f72546b57a3ac9266a62d8c257bee97d85fd078/numpy-2.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4cd9f6fa7ce10dc4627f2bb81dd9075dab67e94632e04c2b638e12575ddaa862", size = 14699514, upload-time = "2026-05-15T20:22:53.678Z" }, + { url = "https://files.pythonhosted.org/packages/47/cc/ddaf3af9c46966fef5be879256f213d85a0c56c75d07a3b7defec7cf6b4c/numpy-2.4.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:4f5bc96d35d94e4ceab8b38a92241b4611e95dc44e63b9f1fa2a331858ee3507", size = 5204601, upload-time = "2026-05-15T20:22:56.257Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/627fadd11959b3c7759008f34c92a35af8ff942dd8284a66ced648bbe516/numpy-2.4.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4bb33e900ee81730ad77a258965134aa8ceac805124f7e5229347beda4b8d0aa", size = 6551360, upload-time = "2026-05-15T20:22:58.334Z" }, + { url = "https://files.pythonhosted.org/packages/a1/47/0728b986b8682d742ff68c16baa5af9d185484abfc635c5cc700f44e62be/numpy-2.4.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32f8f852273ef32b291201ac2a2c97629c4a1ee8632bb670e3443eaa09fc2e72", size = 15671157, upload-time = "2026-05-15T20:23:01.081Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0b/b905ae82d9419dc38123523862db64978ca2954b69609c3ae8fdaca1084c/numpy-2.4.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685681e956fc8dcb75adc6ff26694e1dfd738b24bd8d4696c51ca0110157f912", size = 16645703, upload-time = "2026-05-15T20:23:04.358Z" }, + { url = "https://files.pythonhosted.org/packages/5f/24/e27fc3f5236b4118ed9eed67111675f5c61a07ea333acec87c869c3b359d/numpy-2.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f64dd84b277a737eb59513f6b9bb6195bf41ab11941ef15b2562dbab43fa8ef", size = 17021018, upload-time = "2026-05-15T20:23:07.021Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a7/9041af38d527ab80a06a93570a77e29425b41507ad41f6acf5da78cfb4a4/numpy-2.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b42d9496f79e3a728192f05a42d86e36163217b7cdecb3813d0028a0aa6b72d7", size = 18368768, upload-time = "2026-05-15T20:23:09.44Z" }, + { url = "https://files.pythonhosted.org/packages/49/82/326a014442f32c2663434fd424d9298791f47f8a0f17585ad60519a5606e/numpy-2.4.5-cp312-cp312-win32.whl", hash = "sha256:86d980970f5110595ca14855768073b08585fc1acc36895de303e039e7dee4a5", size = 5962819, upload-time = "2026-05-15T20:23:11.631Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/cbf5d391b0b3a5e8cad264603e2fae256b0bde8ce43566b13b78faedc659/numpy-2.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:3333dba6a4e611d666f69e177ba8fe4140366ff681a5feb2374d3fd4fff3acb6", size = 12321621, upload-time = "2026-05-15T20:23:14.305Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d0/0f18909d9bc37a5f3f969fc737d2bb5df9f2ff295f71b467e6f52a0d6c4e/numpy-2.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:4593d197270b894efeb538dcbe227e4bcf1c77f88c4c6bf933ead812cfaa4453", size = 10221430, upload-time = "2026-05-15T20:23:16.887Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a4/fb50657c7cab297bf34edcd60a074cb0647f61771430d6363575274160fe/numpy-2.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ef248460b645c102026b82337cc4e88231909c66dd77b59ec6d6cac7e44f277", size = 16684760, upload-time = "2026-05-15T20:23:19.436Z" }, + { url = "https://files.pythonhosted.org/packages/3e/43/87e731299b9408eda705b3b9cb31c7bceb9347d2af9cbb16b2b1e4b5bc0f/numpy-2.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4603622bdcdbf8dccb1d9d5b21d16a7aa4e473ae6c8e14048d846fd4ca2907a0", size = 14694117, upload-time = "2026-05-15T20:23:21.832Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/0b2bb8acea222e9dd6e582afc2bc553b89b8833cbdccc68e68f050fb31f8/numpy-2.4.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6c18d49c67689c562854b53fdc433b93e47c12952aa6fa6d59f185e1a5992419", size = 5199141, upload-time = "2026-05-15T20:23:24.066Z" }, + { url = "https://files.pythonhosted.org/packages/39/60/b6972b5d47033d90000f0097c81a98b9486589a2d7003bf725bff275cb0d/numpy-2.4.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b1c663ddc641f4192e90511bec61a09bc231e3bbdb996cdc6edbcaa0e528d685", size = 6546954, upload-time = "2026-05-15T20:23:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e9/ed667cb12c11ca0adde431f685d3a5dd78e6f78b27228c581c8415198e9e/numpy-2.4.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93793222b524f692f12b2f8752ce8b1d9d9125b2bfd5dbf0fb69c92c5e1ce86c", size = 15669430, upload-time = "2026-05-15T20:23:28.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/e5/679f6ffeb01294b0008e5ada4a113cb47617bc0e1819a529fd7973c6d7f4/numpy-2.4.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1616bde34b2bcba2fa9bde06217ce00da4f3d1bdfb264d54525a99e8fe170d83", size = 16633390, upload-time = "2026-05-15T20:23:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/36/46/42bfffc9a780ec902ccd7470d3219192ee82b7b442710307dd85b4d121b0/numpy-2.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09d7d97da1c2c62f4818b3e150a57572ff8dcf1cf5ac501aac832ffd4ebd9566", size = 17020709, upload-time = "2026-05-15T20:23:34.08Z" }, + { url = "https://files.pythonhosted.org/packages/44/00/3e840bfee0cc6cec22209f2c97057f26eeb30de031e4933b4dfc0395416c/numpy-2.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d68d0b355ab2e39fe0de59001d7151dfdbbb880ef67baeed806661e03df5097", size = 18357818, upload-time = "2026-05-15T20:23:36.965Z" }, + { url = "https://files.pythonhosted.org/packages/72/cb/3447b400b9da84134575486f0f656541559b00d4b262477bce9b678bbca8/numpy-2.4.5-cp313-cp313-win32.whl", hash = "sha256:fe28b64777ddfa0eca9b5f51474034ebe3dcb8324f48f27b28f479085673ae33", size = 5961114, upload-time = "2026-05-15T20:23:39.586Z" }, + { url = "https://files.pythonhosted.org/packages/28/f9/a90d2220ffcdc0798f5d55bb5d5463cd6254ec9ef43f384dae80217d7a2f/numpy-2.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:fb4a6c9c537d6ccec9cc4aeae4261bd3cc79b070c67ddc0646f5b1c07fddde42", size = 12318553, upload-time = "2026-05-15T20:23:41.436Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/96f531fb3234545315152d34efdf3de7daee81254448447eb619e8d16967/numpy-2.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:6d7df2da2e7ea0624a43aa368104b3a3ce14aae98ad4bb2c9a93fecef76f1c97", size = 10222200, upload-time = "2026-05-15T20:23:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f4/a291caab5a3c520babf93ff77c54fd5fdb1ebbc3296cee2eb2146ce773b1/numpy-2.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:2a235607a18df941760a695927051af4b1cd5d3ee85840d0e2af816785771feb", size = 14821438, upload-time = "2026-05-15T20:23:45.911Z" }, + { url = "https://files.pythonhosted.org/packages/85/26/13dbb1159b864370568e7309063fd72667984df89db74e9caeb175d067c7/numpy-2.4.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:58dcf64969d870f36bc7fbd557d2617e997db7dc06261b6e3327148ea460d0a4", size = 5326663, upload-time = "2026-05-15T20:23:48.18Z" }, + { url = "https://files.pythonhosted.org/packages/7c/99/d233408072a0e019e2288e27edd23f7d572ccd4a73d1539baa3270ede85d/numpy-2.4.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:235f54b0156274d8fa3155db3ed6d2f401c7e8f3367c90db0a12f02a58fde6ed", size = 6646874, upload-time = "2026-05-15T20:23:49.856Z" }, + { url = "https://files.pythonhosted.org/packages/c5/00/eeb6f193dfe767725e952e0464f3e51f44145c5dd261cd7389aa36ac0713/numpy-2.4.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3b5bb65437a3555c648e706475db01c645559ca80dc8b03e4f202ea757e0d6", size = 15728147, upload-time = "2026-05-15T20:23:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c9/b8ed039f1fde1b13a8807c893e7e2f9432a379f4d6401edecf0028da5b2c/numpy-2.4.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f09a7e5f017d7098c66522097c96257411c9620c0926212200d66bc8cee3976", size = 16681770, upload-time = "2026-05-15T20:23:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/11/5b/0198ef6cb7016eca6d895d392106012138127fab23f46637e76d5e25c9f5/numpy-2.4.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:993a88d8fdd8554466a8765cd8bacd97ba56b70ca6b0a04bcdca77f5afed4222", size = 17086218, upload-time = "2026-05-15T20:23:56.646Z" }, + { url = "https://files.pythonhosted.org/packages/f0/fe/8821f3cfc660ae84c92ee158505941874b62c56a42e035a41425228cd8cf/numpy-2.4.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:84f58bed609b5669f5ad3d597901a4f1f86ee5b3c3708aaa55f05b4fe6e0f656", size = 18403542, upload-time = "2026-05-15T20:23:59.173Z" }, + { url = "https://files.pythonhosted.org/packages/0e/00/e64ecaf498865e7b091f57658b2c522503e5d1b70e43b807f5f8247e1d88/numpy-2.4.5-cp313-cp313t-win32.whl", hash = "sha256:7200c58f3f933ca61e66346667dcc8510bb111995e9ce15398a731e6a4afa4bb", size = 6084903, upload-time = "2026-05-15T20:24:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/20/c0/354997dedaf74e8311c2cf9a6027b476fd8d424cb92189cc0ae2b25f501c/numpy-2.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c26c71080d35db5002102f5d9ff614d45de02aa1f7802943e691e063e5ee93bc", size = 12458420, upload-time = "2026-05-15T20:24:03.735Z" }, + { url = "https://files.pythonhosted.org/packages/66/dc/917ee5ea4a31ca1a6e4c9a85386477efa318dcc60db257c5ef4adda096c1/numpy-2.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:2caa576d1707b275cba1aeb60a5c50daa6fa2a3f28ecb08123bc05fd439005db", size = 10291826, upload-time = "2026-05-15T20:24:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c1/3be0bf102fc17cff5bd142e3be0bfffabec6fa46da0a462396c76b0765d0/numpy-2.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:889ca2c072315de638a5194a772aa1fa2df92bdd6175f6a222d4784040424b61", size = 16683455, upload-time = "2026-05-15T20:24:08.988Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3e/0742d724901fa36bc54b338c6e62e463a7601180da896aa44978f0adf004/numpy-2.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:89e89304fb1f8c3f0ecfa4a7d48f311dd79771336a940e920159d643d1307e77", size = 14704577, upload-time = "2026-05-15T20:24:11.542Z" }, + { url = "https://files.pythonhosted.org/packages/25/1c/196c610ff4c6782d697ba780ebdc1616be143213701bf22c1a270f3bf7dd/numpy-2.4.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:144fcc5a3a17679b2b82543b4a2d8dd29937230a7af13232b5f753872feb6361", size = 5209756, upload-time = "2026-05-15T20:24:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/52/c0/23fb1bc506f774e03db66219a2830e720f4d3dbcaaddf855a7ff7bb6d96f/numpy-2.4.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:398bb16772b265b9fa5c07b07072646ea97137c10ffb62a9a087b277fc825c29", size = 6543937, upload-time = "2026-05-15T20:24:16.223Z" }, + { url = "https://files.pythonhosted.org/packages/9f/49/db4662c26e68520afcc84d672a6f9f5294063dee0e57a46d61afdaa7f9ed/numpy-2.4.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb352e7b8876da1249e72254736d6c58c505fa4e58a3d7e30efca241ca9ca9ce", size = 15685292, upload-time = "2026-05-15T20:24:17.978Z" }, + { url = "https://files.pythonhosted.org/packages/43/80/1315439acedd8398319bac177d6de3d48ab39c62cc0c810f74f0a9a73996/numpy-2.4.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7341b08ff8124d7353939778e2707b8732d03c78c1c30e0815aba2dacbe1245a", size = 16638528, upload-time = "2026-05-15T20:24:20.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/81/364388600932618fe735d97fdd2437cb8dd87a23377ac11d8b9d5db098b7/numpy-2.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:deb01226f012539f3945261ffe1c10aec081a0fa0a5c925419933c70f3ae2d23", size = 17036709, upload-time = "2026-05-15T20:24:22.949Z" }, + { url = "https://files.pythonhosted.org/packages/32/4a/a1185b18a94a6d9587e54b437e7d0ba36ecf6e614f1bea03f5249912c64e/numpy-2.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d888bdf7335f76878c3c7b264ac1ff089863e211ec81249f9fb5795c2183dc25", size = 18363254, upload-time = "2026-05-15T20:24:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8e/95c1d2ed15ae97750ede8c8a0ac487c9c01207afff430f47078b1d9d7dc5/numpy-2.4.5-cp314-cp314-win32.whl", hash = "sha256:15f90d1256e9b2320aff24fde44815b787ab6d7c49a1a11bfd8138b321c5f080", size = 6010184, upload-time = "2026-05-15T20:24:27.852Z" }, + { url = "https://files.pythonhosted.org/packages/aa/92/d063df4d63d988b20d881856c74df76c0c1786229bb870f3a52af0981d4d/numpy-2.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4bd2cd4ef9c0afa87de73723c0a33c0edff62143e1432917458e26d3d195d87f", size = 12450344, upload-time = "2026-05-15T20:24:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/3d/64/c0ae481f7c3b2f85869bcd8fc5d30aa7c96b394162eef9c9315957f115c5/numpy-2.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:db304568c650e9d7039744d3575d0d287754debb2057d7c7b8cdfdc2c487a957", size = 10495674, upload-time = "2026-05-15T20:24:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/57/89/c5a4c677acf17aa50ba09a15e61812f90baac42bb6ca38d112e005858351/numpy-2.4.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de2883e0d2c63eae1bab1a84b390dca74aabb3d20ea1f5d58f360853c83abf3", size = 14824078, upload-time = "2026-05-15T20:24:34.669Z" }, + { url = "https://files.pythonhosted.org/packages/e7/52/57e7144284f6b51ba93523e495ff239260b1ecd5257e3700a436332e5688/numpy-2.4.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:06760fe73ae5005008748d182de612c733542af3cde063d532cd2127561b27be", size = 5329246, upload-time = "2026-05-15T20:24:36.957Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b3/09dbce80fd4a7db4318f2fc01eec0ae76f29306442b5a32d4b811d082cdf/numpy-2.4.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:4b51a01745cb04cc19278482207444b4d30728ce91c28d27a3bfae5fc6ff24c7", size = 6649877, upload-time = "2026-05-15T20:24:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/dbdb23e82d540b757690ef13f011c386fca6a63848eec6136baf8ce7cbed/numpy-2.4.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a05636d7937d0936f271e5ba957fa8d746b5be3c2025caa1a2508f4fe521d40", size = 15730534, upload-time = "2026-05-15T20:24:41.168Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bd/68f6e9b3c20decf40ac06708a7b506757e3a8588efed32988d1b747316be/numpy-2.4.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b86f56048ed09c3bbe48962a7dff077c2fd3274f8cf981800f3b38eac49cc3", size = 16679741, upload-time = "2026-05-15T20:24:44.874Z" }, + { url = "https://files.pythonhosted.org/packages/39/1d/0fcac0b6b4ea1b50ca8fca05a34bed5c8d56e34c1cb5ffb04cf76109ac3c/numpy-2.4.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:130d58151c4db23e9fa860b84784e219a3aa3e030acc88a493ea37006c4dfd4c", size = 17085598, upload-time = "2026-05-15T20:24:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e8/a472b2564cf6cc498ad7aa9741d9832648221b8ab8cc0dbef41faa248ede/numpy-2.4.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d475afc8cbe935ff5944f753d863bba774d7f4e1feaaa4102901e3e053ca5963", size = 18403855, upload-time = "2026-05-15T20:24:50.474Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a4/da82196f8cc4bd28ecf17bd57008c84f3d4696caf06753d9bad45e4ad749/numpy-2.4.5-cp314-cp314t-win32.whl", hash = "sha256:27f4a6dc26353a860b348961b9aa9e009835688b435cfa105e873b8dc2c726f5", size = 6156900, upload-time = "2026-05-15T20:24:53.134Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/860959b91a73d9a085006554fa3850da51a7ffab64599bac5097243438ab/numpy-2.4.5-cp314-cp314t-win_amd64.whl", hash = "sha256:76ac6e90f5e226011c88f9b7040a4bcae612518bc7e9adc127e697a13b28ad1a", size = 12638906, upload-time = "2026-05-15T20:24:55.009Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2a/bbd3097913083ad07c0f28fc9629666221fc18923e17ce97ae22a5dccdd6/numpy-2.4.5-cp314-cp314t-win_arm64.whl", hash = "sha256:7c392e2c1bf596701d3c6832be7567eab5d5b0a13865036c33365ee097d37f8b", size = 10565875, upload-time = "2026-05-15T20:24:57.425Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5d/9a644cfb841bc76b584afc3af1708b3bf6c5cb51fc84a7008246cd93b7b7/numpy-2.4.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6bf0bfc1c2e1db972e30b6cd3d4861f477f3af908b27799b239dc3cbe3eb4b95", size = 16847544, upload-time = "2026-05-15T20:24:59.746Z" }, + { url = "https://files.pythonhosted.org/packages/56/8f/4fe5e3ba76d858dae1fe79078818c0520447335be0082c0dedf82719cc08/numpy-2.4.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:73d664413fb97229149c4711ef56531a6fe8c15c1c2626b0bbe497b84c287e70", size = 14889039, upload-time = "2026-05-15T20:25:03.179Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6f/79f195abf922ecc43e7d0eb6cc969462a71b524a35bcd1fa26b4a1d7406a/numpy-2.4.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:b35bee5ef99e8d227a07829bee2e864fcb65f7c157646fcd8ec8b4b45dd8b88f", size = 5394106, upload-time = "2026-05-15T20:25:05.659Z" }, + { url = "https://files.pythonhosted.org/packages/58/6f/79cd6247205802bcbd10b40ea087e20ded526e10e9be224d34de832b216e/numpy-2.4.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:02981d0fc9f9ce147643d552966d47f329a02f7ecb3b113e84207242f20dfa83", size = 6708718, upload-time = "2026-05-15T20:25:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/d7/22/5f378a9d4633c98f28c4709d4144b1a4630c5c09e109d2e781e2d26c8fe1/numpy-2.4.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e63caf31a1df06338ae63d999f7a33a675ced62eea9c9b02db4b1c1f45cff38", size = 15798292, upload-time = "2026-05-15T20:25:10.689Z" }, + { url = "https://files.pythonhosted.org/packages/63/1c/cec582febef798c99888892d92dc1d28dfe29cb427c41f44d13d0dec208f/numpy-2.4.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8fc52b85a7b45e474be53eddf08e006d22e381a4e41bcde8e4aa08da0e7d198", size = 16747406, upload-time = "2026-05-15T20:25:13.879Z" }, + { url = "https://files.pythonhosted.org/packages/b1/dc/d358a16a6fec86cf736b8fbe67386044b3fa2aded1a80cff90e836799301/numpy-2.4.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:40c71d50a4da1a7c317af419461052d3911a5770bfc5fd55baf52cc45e7a2c20", size = 12504085, upload-time = "2026-05-15T20:25:16.667Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" }, + { url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" }, + { url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" }, + { url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" }, + { url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" }, + { url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" }, + { url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" }, + { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, +] + +[[package]] +name = "spinterp" +source = { editable = "." } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "zensical" }, +] +docs = [ + { name = "zensical" }, +] +lint = [ + { name = "ruff" }, +] +test = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, +] + +[package.metadata] +requires-dist = [{ name = "numpy" }] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, + { name = "ruff", specifier = "==0.15.*" }, + { name = "zensical", specifier = ">=0.0.42" }, +] +docs = [{ name = "zensical", specifier = ">=0.0.42" }] +lint = [{ name = "ruff", specifier = "==0.15.*" }] +test = [ + { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "zensical" +version = "0.0.42" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/dd/04e89ab92aed1ef9e36c76ef095fb587ffcbe4162aa7f3fe6d63aafade4a/zensical-0.0.42.tar.gz", hash = "sha256:cc346b833868a59412fe8d8498a152be90be9f3d8fb87e1f1a1c2e1146cbae1b", size = 3931093, upload-time = "2026-05-15T10:22:45.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/19/2ca4e52769307959f7485d4c5da7b24787339787c1cbc371885cef448e50/zensical-0.0.42-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bffd7a34b570fa3ccadf1d23babb0f7c4851c6b626e4fc8ed9f21c2eaae85968", size = 12705326, upload-time = "2026-05-15T10:22:07.905Z" }, + { url = "https://files.pythonhosted.org/packages/2c/82/0832b0d2c0c2800174141d5519a017105d3dace9194e2c29730e7a676adf/zensical-0.0.42-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ee1a79789f9462ef44a4b6ebbfc8b5bf4b2447607da8bc5b35bc9c4ce4ea2370", size = 12568663, upload-time = "2026-05-15T10:22:11.072Z" }, + { url = "https://files.pythonhosted.org/packages/ac/87/272b3998322958ca38f09323d2347cb121dfc851477c36962b71319242a5/zensical-0.0.42-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e9a5d508ce8d1b07d8417f0623be476f6b37d445ab4356481a71e613a7979d6", size = 12948460, upload-time = "2026-05-15T10:22:13.792Z" }, + { url = "https://files.pythonhosted.org/packages/ae/1b/e5f153401f162f48cae2d58e96b95fd39ba5bd1728fb5881a60e502f4e1d/zensical-0.0.42-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fbc0951a676e48afe7df3a9b2a30958dcf9c426ed2480972d3c04d6de485ba3", size = 12913460, upload-time = "2026-05-15T10:22:16.791Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5186b4204bdfdf132851b7515a37b9602bfc153fb601db5fb244339bae52/zensical-0.0.42-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f0e96e53f39b9e4b929a25d9df70bd7fa8217166a854e2c8f3185983dd01500", size = 13276704, upload-time = "2026-05-15T10:22:19.819Z" }, + { url = "https://files.pythonhosted.org/packages/f2/df/b57b5fcc631ac7a4b4c6834d8cf0b88d3fca37c9db42fc6bbf9f097200ed/zensical-0.0.42-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7d586e57436d603e88acd856864f99f0771aef24bf6560b2de238417bd3817c", size = 12987069, upload-time = "2026-05-15T10:22:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3a/b326a44a065d98e89b472645ad33037201e3385340c2e6e35627b18ab3fa/zensical-0.0.42-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3c026f023330d67f986a94b68ffd36dc5066882e697e1125c37308d8d684135c", size = 13124195, upload-time = "2026-05-15T10:22:25.543Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1e/823740a662e357a8826dc8eeb87e06705e64219b2774430bc555f7c53d57/zensical-0.0.42-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e5908bc09cf5c1c50c9504241e37f89955daf3e89ba1b9d71c17972578b24804", size = 13182981, upload-time = "2026-05-15T10:22:28.89Z" }, + { url = "https://files.pythonhosted.org/packages/80/6d/9fe261267ac36a7d57051d790022408e9043bc925c9ad21971a1e5b6c3e8/zensical-0.0.42-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:c0bf96b55f0a44e8716bcb334a16380ed56772b555145da775a7d8ac8678cb6f", size = 13332666, upload-time = "2026-05-15T10:22:32.249Z" }, + { url = "https://files.pythonhosted.org/packages/9b/57/9b0e4f131a7ad15cf1aca081748ea7336c084fb8e16be202a6bed32f595c/zensical-0.0.42-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:47cd99583738a8ab03fac4080741275c56e741a06dc8edfb541f4c1649a5ae69", size = 13270817, upload-time = "2026-05-15T10:22:35.388Z" }, + { url = "https://files.pythonhosted.org/packages/bb/fd/bdb85cc444e4146e8970a22e48a903bfed5bf83276ad7d755caa415dda64/zensical-0.0.42-cp310-abi3-win32.whl", hash = "sha256:83090e53fba061967ecb3dff81500b1900f288bae108bf54084a2aeb6648ebd0", size = 12256227, upload-time = "2026-05-15T10:22:38.869Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b9/09d1f735c8e6d3eb61d176ed5ebcf658b65b126d7d4bbc03a7d366a1e17d/zensical-0.0.42-cp310-abi3-win_amd64.whl", hash = "sha256:2e4304e103f9cd5c637045bbae1ff29de3009ab01b16e99c2fd6d4bbceb7a3ee", size = 12486598, upload-time = "2026-05-15T10:22:42.158Z" }, +]