Update READMEs & add SECURITY.md (#78)
jump to
@@ -1,3 +1,3 @@
-[submodule "poulpy-backend/src/implementation/cpu_spqlios/spqlios-arithmetic"] - path = poulpy-backend/src/implementation/cpu_spqlios/spqlios-arithmetic +[submodule "poulpy-backend/src/cpu_spqlios/spqlios-arithmetic"] + path = poulpy-backend/src/cpu_spqlios/spqlios-arithmetic url = https://github.com/phantomzone-org/spqlios-arithmetic
@@ -353,25 +353,7 @@ "byteorder",
"cmake", "criterion", "itertools 0.14.0", - "poulpy-hal 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand", - "rand_chacha", - "rand_core", - "rand_distr", - "rug", -] - -[[package]] -name = "poulpy-backend" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c6c0ad35bd5399e72a7d51b8bad5aa03e54bfd63bf1a09c4a595bd51145ca6" -dependencies = [ - "byteorder", - "cmake", - "criterion", - "itertools 0.14.0", - "poulpy-hal 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "poulpy-hal", "rand", "rand_chacha", "rand_core",@@ -386,22 +368,8 @@ dependencies = [
"byteorder", "criterion", "itertools 0.14.0", - "poulpy-backend 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "poulpy-hal 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rug", -] - -[[package]] -name = "poulpy-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34afc307c185e288395d9f298a3261177dc850229e2bd6d53aa4059ae7e98cab" -dependencies = [ - "byteorder", - "criterion", - "itertools 0.14.0", - "poulpy-backend 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "poulpy-hal 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "poulpy-backend", + "poulpy-hal", "rug", ]@@ -421,31 +389,14 @@ "rug",
] [[package]] -name = "poulpy-hal" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63312a7be7c5fd91e1f5151735d646294a4592d80027d8e90778076b2070a0ec" -dependencies = [ - "byteorder", - "cmake", - "criterion", - "itertools 0.14.0", - "rand", - "rand_chacha", - "rand_core", - "rand_distr", - "rug", -] - -[[package]] name = "poulpy-schemes" version = "0.1.1" dependencies = [ "byteorder", "itertools 0.14.0", - "poulpy-backend 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "poulpy-core 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "poulpy-hal 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "poulpy-backend", + "poulpy-core", + "poulpy-hal", ] [[package]]
@@ -7,22 +7,20 @@ </p>
[](https://github.com/phantomzone-org/poulpy/actions/workflows/ci.yml) -**Poulpy** is a fast & modular FHE library that implements Ring-Learning-With-Errors based homomorphic encryption. It adopts the bivariate polynomial representation proposed in [Revisiting Key Decomposition Techniques for FHE: Simpler, Faster and More Generic](https://eprint.iacr.org/2023/771). In addition to simpler and more efficient arithmetic than the residue number system (RNS), this representation provides a common plaintext space for all schemes and allows easy switching between any two schemes. Poulpy also decouples the schemes implementations from the polynomial arithmetic backend by being built around a hardware abstraction layer (HAL). This enables user to easily provide or use a custom backend. +**Poulpy** is a fast & modular FHE library that implements Ring-Learning-With-Errors based homomorphic encryption. It adopts the bivariate polynomial representation proposed in [Revisiting Key Decomposition Techniques for FHE: Simpler, Faster and More Generic](https://eprint.iacr.org/2023/771). In addition to simpler and more efficient arithmetic than the residue number system (RNS), this representation provides a common plaintext space for all schemes and native bridges between any two schemes. Poulpy also decouples the schemes implementations from the polynomial arithmetic backend by being built from the ground up around a **hardware abstraction layer** that closely matches the API of [spqlios-arithmetic](https://github.com/tfhe/spqlios-arithmetic). Leveraging HAL library users can develop applications while being generic over the backend and choose any backend of choice at runtime. -## Library Overview +<p align="center"> +<img src="docs/lib_diagram.png" /> +</p> -- **`poulpy-hal`**: a crate providing layouts and a trait-based hardware acceleration layer with open extension points, matching the API and types of spqlios-arithmetic. - - **`api`**: fixed public low-level polynomial level arithmetic API closely matching spqlios-arithmetic. - - **`delegates`**: link between the user facing API and implementation OEP. Each trait of `api` is implemented by calling its corresponding trait on the `oep`. - - **`layouts`**: layouts of the front-end algebraic structs matching spqlios-arithmetic types, such as `ScalarZnx`, `VecZnx` or opaque backend prepared struct such as `SvpPPol` and `VmpPMat`. - - **`oep`**: open extension points, which can be (re-)implemented by the user to provide a concrete backend. - - **`tests`**: backend agnostic & generic tests for the OEP/layouts. -- **`poulpy-backend`**: a crate providing concrete implementations of **`poulpy-hal`**. - - **`cpu_spqlios`**: cpu implementation of **`poulpy-hal`** through the `oep` using bindings on spqlios-arithmetic. This implementation currently supports the `FFT64` backend and will be extended to support the `NTT120` backend once it is available in spqlios-arithmetic. -- **`poulpy-core`**: a backend agnostic crate implementing scheme agnostic RLWE arithmetic for LWE, GLWE, GGLWE and GGSW ciphertexts using **`poulpy-hal`**. -- **`poulpy-schemes`**: a backend agnostic crate implementing mainstream FHE schemes using **`poulpy-core`** and **`poulpy-hal`**. +## Library Crates -### Bivariate Polynomial Representation +- **`poulpy-hal`**: a crate providing layouts and a trait-based hardware acceleration layer with open extension points, matching the API and types of spqlios-arithmetic. This crate does not provide concrete implementations other than the layouts (e.g. `VecZnx`, `VmpPmat`). +- **`poulpy-core`**: a backend agnostic crate implementing scheme agnostic RLWE arithmetic for LWE, GLWE, GGLWE and GGSW ciphertexts using **`poulpy-hal`**. Can be instantiated with any backend provided by **`poulpy-backend`**. +- **`poulpy-schemes`**: a backend agnostic crate implementing mainstream FHE schemes using **`poulpy-core`** and **`poulpy-hal`**. The crate can be instantiated with any backend provided by **`poulpy-backend`**. +- **`poulpy-backend`**: a crate providing concrete implementations of **`poulpy-hal`** for various representations and hardwares. + +## Bivariate Polynomial Representation Existing FHE implementations (such as [Lattigo](https://github.com/tuneinsight/lattigo) or [OpenFHE](https://github.com/openfheorg/openfhe-development)) use the [residue-number-system](https://en.wikipedia.org/wiki/Residue_number_system) (RNS) to represent large integers. Although the parallelism and carry-less arithmetic provided by the RNS representation provides a very efficient modular arithmetic over large-integers, it suffers from various drawbacks when used in the context of FHE. The main idea behind the bivariate representation is to decouple the cyclotomic arithmetic from the large number arithmetic. Instead of using the RNS representation for large integer, integers are decomposed in base $2^{-K}$ over the Torus $\mathbb{T}_{N}[X]$.@@ -36,21 +34,17 @@ - **Linear number of DFT in the half external product:** The bivariate representation of the coefficients implicitly provides the digit decomposition, as such the number of DFT is linear in the number of limbs, contrary to the RNS representation where it is quadratic due to the RNS basis conversion. This enables a much more efficient key-switching, which is the **most used and expensive** FHE operation.
- **Unified plaintext space:** The bivariate polynomial representation is by essence a high precision discretized representation of the Torus $\mathbb{T}_{N}[X]$. Using the Torus as the common plaintext space for all schemes achieves the vision of [CHIMERA: Combining Ring-LWE-based Fully Homomorphic Encryption Schemes](https://eprint.iacr.org/2018/758) which is to unify all RLWE-based FHE schemes (TFHE, FHEW, BGV, BFV, CLPX, GBFV, CKKS, ...) under a single scheme with different encodings, enabling native and efficient scheme-switching functionalities. -- **Simpler implementation**: Since the cyclotomic arithmetic is decoupled from the coefficient representation, the same pipeline (including DFT) can be reused for all limbs (unlike in the RNS representation), making this representation a prime target for hardware acceleration. +- **Simpler implementation**: Since the cyclotomic arithmetic is decoupled from the coefficient representation, the same pipeline (including DFT) can be reused for all limbs (unlike in the RNS representation). The bivariate representation also has straight forward flat, aligned & vectorized memory layout. All these aspect make this representation a prime target for hardware acceleration. - **Deterministic computation**: Although being defined on the Torus, bivariate arithmetic remains integer polynomial arithmetic, ensuring all computations are deterministic, the contract being that output should be reproducible and identical, regardless of the backend or hardware. - -### Hardware Abstraction Layer - -In addition to providing a general purpose FHE library over a unified plaintext space, Poulpy is also designed from the ground up around a **hardware abstraction layer** that closely matches the API of [spqlios-arithmetic](https://github.com/tfhe/spqlios-arithmetic). The bivariate representation is by itself hardware friendly as it uses flat, aligned & vectorized memory layout. Finally, generic opaque write only structs (prepared versions) are provided, making it easy for developers to provide hardware focused/optimized operations. This makes possible for anyone to provide or use a custom backend. ## Installation -- **`poulpy-hal`**: https://crates.io/crates/poulpy-hal/0.1.0 -- **`poulpy-backend`**: https://crates.io/crates/poulpy-backend/0.1.0 -- **`poulpy-core`**: https://crates.io/crates/poulpy-core/0.1.0 -- **`poulpy-schemes`**: https://crates.io/crates/poulpy-schemes/0.1.0 -- +- **`poulpy-hal`**: https://crates.io/crates/poulpy-hal +- **`poulpy-core`**: https://crates.io/crates/poulpy-core +- **`poulpy-schemes`**: https://crates.io/crates/poulpy-schemes +- **`poulpy-backend`**: https://crates.io/crates/poulpy-backend + ## Documentation * Full `cargo doc` documentation is coming soon.
@@ -0,0 +1,29 @@
+# 🔒 Security Policy for Poulpy + +## Report a Vulnerability + +To report a vulnerability, please contact us at: **[jean-philippe@phantom.zone](mailto:jean-philippe@phantom.zone)** + +Include in your report (if possible): + +* Affected crate +* Steps to reproduce +* Impact +* Potential fix + +We will acknowledge receipt and work with you on resolution. + +## Security Model + +Poulpy implements RLWE-based cryptography and follows the standard **IND-CPA security model** when used with appropriate parameters. + +To select secure parameters, we recommend using the [Lattice Estimator](https://github.com/malb/lattice-estimator). + +Like other FHE libraries, Poulpy does **not** provide stronger security notions out of the box and users should be aware that: + +* FHE ciphertexts are malleable by design and are not inherently CCA-secure. +* Circuit privacy is not guaranteed without additional techniques (e.g., re-randomization, modulus switching, noise flooding). + +Users should therefore design protocols accordingly and apply standard safeguards (e.g., private & authenticated channels, key rotation, limiting decryption queries). + +Additional context on security notions beyond CPA can be found in [Relations among new CCA security notions for approximate FHE](https://eprint.iacr.org/2024/812).
@@ -0,0 +1,763 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + width="100%" + height="100%" + viewBox="0 0 2481 3508" + version="1.1" + xml:space="preserve" + style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;" + id="svg64" + sodipodi:docname="poulpy Tentacules spirales.svg" + inkscape:version="1.4 (86a8ad7, 2024-10-11)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"><defs + id="defs64"><linearGradient + id="swatch64" + inkscape:swatch="solid"><stop + style="stop-color:#800000;stop-opacity:1;" + offset="0" + id="stop64" /></linearGradient></defs><sodipodi:namedview + id="namedview64" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + inkscape:zoom="0.10930144" + inkscape:cx="-279.04482" + inkscape:cy="1880.1217" + inkscape:window-width="1440" + inkscape:window-height="795" + inkscape:window-x="13" + inkscape:window-y="25" + inkscape:window-maximized="0" + inkscape:current-layer="g62" /> + <g + transform="translate(-115.25321,-14.010778)" + id="g1" + style="display:inline" + inkscape:label="Cercle violet"> + <circle + cx="1340.76" + cy="1684.3199" + r="1162.45" + style="display:inline;fill:#5f6298" + id="circle1" /> + </g><g + transform="translate(-6.4751427,-34.929023)" + id="g20" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5" + inkscape:label="fond noir 1"> + <path + d="m 1260.3865,739.845 c 5.29,-0.046 10.41,0.05 15.35,0.288 495.18,23.94 581.34,498.047 543.52,612.187 -50.28,151.75 -171.06,161.25 -171.06,161.25 0,0 -12.7207,78.7444 55.0716,148.6216 56.6514,58.3937 132.6911,76.6372 214.3562,69.7163 58.5899,-4.9653 93.766,-68.1334 71.0118,-75.374 -9.0175,-2.8694 -21.9482,10.8239 -27.8588,10.2112 -23.8267,-2.2232 -12.8242,-56.4314 21.7222,-71.7607 62.0304,-24.0788 101.5395,54.2671 84.9164,117.7264 -11.5498,44.0917 -62.7508,109.0407 -165.3601,142.645 -218.5481,61.0039 -356.2894,-110.0748 -356.2894,-110.0748 0,0 -1.1178,-12.2774 -23.626,23.3939 -16.7075,26.4783 -28.1841,64.529 -11.0031,118.4131 7.5413,23.6515 43.7984,89.7393 124.914,95.411 48.3483,3.3806 60.7328,0.061 77.2169,-12.9753 6.4458,-5.0977 11.114,-14.831 5.8663,-20.5187 -5.2477,-5.6877 -10.9355,-5.9254 -15.7542,-4.4325 -11.0694,3.4295 -12.174,15.6315 -19.2241,6.2027 -3.6427,-4.8717 -2.0722,-13.41 1.7178,-18.1679 10.1145,-12.6974 28.9112,-19.4543 46.2318,-15.3089 14.4706,3.4634 35.0898,9.3967 39.7211,20.9893 19.5576,48.9539 -2.8091,115.7527 -80.7535,137.5537 -149.1647,41.7211 -297.1506,-65.2398 -354.0202,-176.9794 -65.9569,-129.5946 -21.5901,-188.584 -21.5901,-188.584 0,0 28.0808,-83.3759 18.4194,-53.4325 -9.6614,29.9434 -75.9266,196.5661 -140.516,277.779 -35.5921,44.7525 -74.9884,80.2221 -131.7121,109.9007 -87.90646,45.9938 -158.69557,51.9736 -236.91678,35.8278 -42.69884,-8.8136 -90.26739,-42.0136 -111.01277,-69.1664 -34.84263,-45.6041 -17.68081,-101.8993 17.74403,-119.466 20.75728,-10.2933 38.26636,-9.2394 54.54505,-1.1326 11.2662,5.6106 24.93011,16.7866 22.12078,30.5994 -1.55625,7.6517 -13.77512,-17.9241 -17.78841,-11.227 -6.58335,10.9859 0.7482,33.3981 -5.87782,29.0793 -8.98727,-5.8579 -23.69361,-20.7044 -34.66215,0.8708 -13.31668,26.194 17.08627,52.2373 44.5459,59.0755 19.99027,4.9781 60.32342,10.5607 109.31696,-8.7342 80.33381,-31.6376 110.99071,-96.7476 127.57411,-161.0739 30.2577,-117.3682 -7.9568,-120.1084 -7.9568,-120.1084 0,0 -75.52237,133.9789 -240.92448,146.2884 -172.75646,12.8568 -296.43122,-99.2234 -327.77517,-166.8476 -40.6785,-87.7634 -4.3468,-140.7103 46.04996,-160.9368 27.91776,-11.2047 58.87134,-5.8098 77.97151,21.1604 6.55969,9.2626 -14.99666,12.1594 -19.51243,25.1449 -4.51576,12.9854 4.92601,1.375 -2.50233,-6.1947 -7.78647,-7.9346 2.00409,19.2286 -8.51436,3.4151 -7.3052,-10.9827 -38.12569,-12.7752 -47.97654,20.8356 -6.73059,22.9646 7.9355,59.2194 46.23947,85.9592 38.30397,26.7398 107.01031,50.6937 182.62317,42.7638 155.22443,-23.9486 141.65421,-207.1678 141.65421,-207.1678 0,0 -120.782,-9.5 -171.057,-161.24 -37.817,-114.15 48.338,-588.252 543.52399,-612.192 5.54,-0.268 11.32,-0.356 17.3,-0.264 z" + style="display:inline;fill:#000000;fill-opacity:1" + id="path20" + sodipodi:nodetypes="ccccsssccsccsssszssassssczasassassssssscssssssssscccscc" /> + </g> + <g + transform="translate(-63.05022,-317.2944)" + id="g62" + style="display:inline" + inkscape:label="Tout le poulpe"> + + <g + transform="translate(54.794008,260.88363)" + id="g3" + style="display:inline" + inkscape:label="fond rose"> + <path + d="m 1251.32,739.845 c 5.29,-0.046 10.41,0.05 15.35,0.288 495.18,23.94 581.34,498.047 543.52,612.187 -50.28,151.75 -171.06,161.25 -171.06,161.25 0,0 -12.7207,78.7444 55.0716,148.6216 56.6514,58.3937 127.6365,101.405 209.3016,94.4841 58.5899,-4.9653 130.862,-66.8415 99.5278,-93.7683 -21.3283,-18.3283 -34.9999,19.2513 -51.0811,17.5546 -23.8267,-2.2232 -15.0501,-47.3004 19.4963,-62.6297 62.0304,-24.0788 101.5395,35.3928 86.9032,94.8785 -10.8899,44.2593 -62.7508,109.0407 -165.3601,142.645 -218.5481,61.0039 -356.2894,-110.0748 -356.2894,-110.0748 0,0 -67.0103,66.5229 -34.6291,141.807 9.8088,22.8047 34.0606,83.1622 110.9802,109.5306 71.8346,24.6252 116.5487,-1.5367 118.5063,-25.3422 1.5464,-18.8053 -11.4245,-26.6811 -25.9661,-23.3155 -13.3082,3.0802 -10.7314,17.4612 -21.9548,18.7682 -17.2975,2.0143 -11.4165,-21.1055 -6.6453,-30.2667 7.6796,-14.7457 28.7276,-23.3094 46.0482,-19.164 14.4706,3.4634 31.0113,14.6644 39.4074,31.9041 23.0818,47.3942 -2.4954,104.8379 -80.4398,126.6389 -149.1647,41.7211 -297.1506,-65.2398 -354.0202,-176.9794 -65.9569,-129.5946 -23.2512,-189.2485 -23.2512,-189.2485 0,0 11.8247,-28.0168 2.1633,1.9266 -9.6614,29.9434 -68.6556,157.5877 -122.5988,223.0844 -36.3518,44.1376 -74.9884,80.2221 -131.7121,109.9007 -87.90647,45.9938 -158.69558,51.9736 -236.91679,35.8278 -42.69884,-8.8136 -90.26739,-42.0136 -111.01277,-69.1664 -34.84263,-45.6041 -17.68081,-101.8993 17.74403,-119.466 20.75728,-10.2933 37.80301,-8.2333 54.54505,-1.1326 12.80701,5.4318 23.79285,17.2709 28.37942,30.5994 2.09463,6.087 3.47725,16.7194 -0.53604,23.4165 -6.58335,10.9859 -18.36432,11.3465 -22.65504,2.6393 -11.14741,-22.6214 -30.4274,-28.9079 -41.39594,-7.3327 -13.31668,26.194 17.08627,52.2373 44.5459,59.0755 19.99027,4.9781 60.32342,10.5607 109.31696,-8.7342 80.33381,-31.6376 110.99072,-96.7476 127.57412,-161.0739 30.2577,-117.3682 -7.9568,-120.1084 -7.9568,-120.1084 0,0 -75.52238,133.9789 -240.92449,146.2884 -172.75646,12.8568 -296.43122,-99.2234 -327.77517,-166.8476 -40.6785,-87.7634 -4.3468,-140.7103 46.04996,-160.9368 27.91776,-11.2047 58.87134,-5.8098 77.97151,21.1604 6.55969,9.2626 8.89716,27.5748 4.38139,40.5603 -4.51576,12.9854 -15.88474,20.6442 -23.31308,13.0745 -7.78647,-7.9346 -1.07898,-15.456 -11.59743,-31.2695 -7.3052,-10.9827 -38.12569,-12.7752 -47.97654,20.8356 -6.73059,22.9646 7.9355,59.2194 46.23947,85.9592 38.30397,26.7398 107.01031,50.6937 182.62317,42.7638 C 875.12322,1696.7092 861.553,1513.49 861.553,1513.49 c 0,0 -120.782,-9.5 -171.057,-161.24 -37.817,-114.15 48.338,-588.252 543.524,-612.192 5.54,-0.268 11.32,-0.356 17.3,-0.264 z" + style="display:inline;fill:#c45c8c" + id="path2" + sodipodi:nodetypes="ccccsssccsccsssssassssczasassassssssscssssssssscccscc" /> + </g> + <g + transform="translate(-52.697054,124.07644)" + id="g9" + style="display:inline" + inkscape:label="oeil droite"> + <g + transform="matrix(1.17499,0,0,1.07176,-113.491,52.028)" + id="g4"> + <path + d="m 1037.44,1136.89 c 53.76,-13.17 99.79,59.75 99.79,133.35 0,73.6 -27.99,121.9 -98.95,121.4 -70.921,-0.51 -104.748,-47.94 -100.618,-121.4 5.845,-103.95 69.758,-125.99 99.778,-133.35 z" + style="fill:#080605" + id="path3" /> + </g> + <g + id="g8"> + <g + transform="matrix(1.10138,0,0,1,-29.4601,132.584)" + id="g5"> + <path + d="m 1037.44,1136.89 c 53.76,-13.17 99.79,59.75 99.79,133.35 0,73.6 -27.99,121.9 -98.95,121.4 -70.921,-0.51 -104.748,-47.94 -100.618,-121.4 5.845,-103.95 69.758,-125.99 99.778,-133.35 z" + style="fill:#ffffff" + id="path4" /> + </g> + <g + transform="matrix(1.12222,0,0,1.14583,-142.446,-204.304)" + id="g6"> + <ellipse + cx="1127.84" + cy="1413.55" + rx="82.768997" + ry="88.287003" + style="fill:#080605;stroke:#ffffff;stroke-width:0.88px" + id="ellipse5" /> + </g> + <g + transform="translate(6.29175,1.51796)" + id="g7"> + <ellipse + cx="1140.1801" + cy="1344.67" + rx="30.655001" + ry="26.976999" + style="fill:#ffffff" + id="ellipse6" /> + </g> + <ellipse + cx="1093.8199" + cy="1477.01" + rx="15.634" + ry="13.795" + style="fill:#ffffff" + id="ellipse7" /> + </g> + </g> + <g + transform="translate(69.576436,116.68233)" + id="g16" + style="display:inline" + inkscape:label="oeil gauche"> + <g + transform="matrix(-1.17499,0,0,1.07176,2715.59,52.028)" + id="g10"> + <path + d="m 1037.44,1136.89 c 53.76,-13.17 99.79,59.75 99.79,133.35 0,73.6 -27.99,121.9 -98.95,121.4 -70.921,-0.51 -104.748,-47.94 -100.618,-121.4 5.845,-103.95 69.758,-125.99 99.778,-133.35 z" + style="fill:#080605" + id="path9" /> + </g> + <g + id="g15"> + <g + transform="matrix(-1.10138,0,0,1,2629.72,132.584)" + id="g11"> + <path + d="m 1037.44,1136.89 c 53.76,-13.17 99.79,59.75 99.79,133.35 0,73.6 -27.99,121.9 -98.95,121.4 -70.921,-0.51 -104.748,-47.94 -100.618,-121.4 5.845,-103.95 69.758,-125.99 99.778,-133.35 z" + style="fill:#ffffff" + id="path10" /> + </g> + <g + transform="matrix(1.12222,0,0,1.14583,212.543,-204.304)" + id="g12"> + <ellipse + cx="1127.84" + cy="1413.55" + rx="82.768997" + ry="88.287003" + style="fill:#080605;stroke:#ffffff;stroke-width:0.88px" + id="ellipse11" /> + </g> + <g + transform="translate(318.429,-2.77379)" + id="g13"> + <ellipse + cx="1140.1801" + cy="1344.67" + rx="30.655001" + ry="26.976999" + style="fill:#ffffff" + id="ellipse12" /> + </g> + <g + transform="translate(343.953,-3.67864)" + id="g14"> + <ellipse + cx="1093.8199" + cy="1477.01" + rx="15.634" + ry="13.795" + style="fill:#ffffff" + id="ellipse13" /> + </g> + </g> + </g> + <g + transform="translate(58.789567,275.59801)" + id="g17" + style="display:inline" + inkscape:label="Bouche"> + <path + d="m 1184.15,1386.92 c -1.96,-4.56 0.14,-9.85 4.7,-11.82 4.56,-1.96 9.85,0.14 11.81,4.7 0,0 7.54,17.45 22.04,30.69 8.94,8.15 20.65,14.81 35.16,13.57 14.9,-1.27 26.32,-8.87 34.73,-17.4 13.22,-13.4 19.24,-29.23 19.24,-29.23 1.75,-4.65 6.94,-6.99 11.58,-5.24 4.65,1.75 6.99,6.94 5.24,11.59 0,0 -7.22,19.25 -23.26,35.52 -11.11,11.25 -26.33,21.01 -46,22.68 -20.07,1.71 -36.46,-6.94 -48.81,-18.21 -17.41,-15.89 -26.43,-36.85 -26.43,-36.85 z" + style="fill:#080605" + id="path16" /> + </g> + <ellipse + cx="1322.8101" + cy="1107.62" + rx="40.465" + ry="23.910999" + style="display:inline;fill:#ee7db1" + id="ellipse17" /> + <g + transform="matrix(0.613636,0,0,0.769231,554.31,185.71)" + id="g18" + style="display:inline"> + <ellipse + cx="1322.8101" + cy="1107.62" + rx="40.465" + ry="23.910999" + style="fill:#ee7db1" + id="ellipse18" /> + </g> + <ellipse + cx="1151.76" + cy="1131.53" + rx="29.429001" + ry="16.554001" + style="fill:#ee7db1" + id="ellipse19" /> + <g + transform="matrix(1,0,0,1.34483,0,-375.155)" + id="g19" + style="display:inline"> + <path + d="m 1254.99,1024.64 c 26.69,3.76 45.75,13.56 45.75,25.04 0,14.72 -31.32,26.67 -69.89,26.67 -38.58,0 -69.9,-11.95 -69.9,-26.67 0,-6.31 5.76,-12.11 15.38,-16.68 24.54,-3.87 50.81,-6.81 78.66,-8.36 z" + style="fill:#ee7db1" + id="path19" /> + </g> + <ellipse + cx="1481.91" + cy="1142.5699" + rx="30.349001" + ry="23.910999" + style="display:inline;fill:#ee7db1" + id="ellipse20" /> + <ellipse + cx="1382.59" + cy="1210.62" + rx="19.313" + ry="16.554001" + style="display:inline;fill:#ee7db1" + id="ellipse21" /> + <g + transform="matrix(0.833333,0,0,0.805556,84.6654,270.805)" + id="g22" + style="display:inline"> + <ellipse + cx="1382.59" + cy="1210.62" + rx="19.313" + ry="16.554001" + style="fill:#ee7db1" + id="ellipse22" /> + </g> + <ellipse + cx="1487.4301" + cy="1228.09" + rx="30.349001" + ry="10.116" + style="display:inline;fill:#ee7db1" + id="ellipse23" /> + <ellipse + cx="1095.65" + cy="1225.34" + rx="32.188" + ry="16.554001" + style="display:inline;fill:#ee7db1" + id="ellipse24" /> + <ellipse + cx="1243.72" + cy="1166.48" + rx="23.910999" + ry="14.715" + style="display:inline;fill:#ee7db1" + id="ellipse25" /> + <ellipse + cx="1023" + cy="1160.04" + rx="45.983002" + ry="26.67" + style="display:inline;fill:#ee7db1" + id="ellipse26" /> + <g + transform="translate(0,-5)" + id="g26" + style="display:inline"> + <path + d="m 1107.98,1036.85 c 0.96,3.25 1.47,6.62 1.47,10.07 0,26.39 -29.67,47.82 -66.21,47.82 -15.34,0 -29.47,-3.77 -40.7,-10.11 25.72,-14.96 61.37,-32.8 105.44,-47.78 z" + style="fill:#ee7db1" + id="path26" /> + </g> + <g + transform="translate(0,-4)" + id="g27" + style="display:inline"> + <path + d="m 1563.62,1067.51 c -13.42,17.43 -37.59,29.07 -65.15,29.07 -42.13,0 -76.34,-27.2 -76.34,-60.7 0,-5.79 1.03,-11.4 2.94,-16.71 54.09,12.07 101.59,30.37 138.55,48.34 z" + style="fill:#ee7db1" + id="path27" /> + </g> + <g + transform="translate(4)" + id="g28" + style="display:inline"> + <path + d="m 1703.42,1170.82 c -1.86,18.12 -23.75,32.44 -50.45,32.44 -27.92,0 -50.58,-15.66 -50.58,-34.94 0,-19.29 22.66,-34.95 50.58,-34.95 4.18,0 8.25,0.35 12.15,1.01 14.63,12.74 27.66,25.41 38.3,36.44 z" + style="fill:#ee7db1" + id="path28" /> + </g> + <g + transform="matrix(1.0347689,-0.22534534,0.19650845,1.0734479,-397.60471,95.96781)" + id="g29" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:10.27px" + id="path29" /> + </g><g + transform="matrix(0.02170046,1.0587995,-1.0911959,-0.01405006,3746.0361,947.91497)" + id="g29-6" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:10.27px" + id="path29-5" /> + </g> + <g + transform="rotate(-40.906071,1160.177,1531.1139)" + id="g30" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:10.27px" + id="path30" /> + </g> + <g + transform="matrix(-0.3966518,-0.6663756,0.6663756,-0.3966518,815.69311,3506.6215)" + id="g31" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:13.24px" + id="path31" + sodipodi:nodetypes="ccccc" /> + </g> + <g + transform="matrix(-0.352104,-0.527856,0.527856,-0.352104,935.174,3195.12)" + id="g32" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="display:none;fill:#ee7db1;stroke:#ee7db1;stroke-width:16.19px" + id="path32" + sodipodi:nodetypes="ccccc" /> + </g> + <g + transform="matrix(-0.37808612,-0.44400444,0.49505952,-0.33909441,1192.6929,3196.3183)" + id="g33" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:19.8px" + id="path33" + sodipodi:nodetypes="ccccc" /> + </g> + <g + transform="matrix(-0.316263,-0.391079,0.39830824,-0.31052287,1389.387,3096.7928)" + id="g34" + style="display:inline"> + <path + d="m 983.37947,1928.0267 c -9.95151,10.6496 -9.25774,27.9137 0.4238,34.6308 11.78749,8.1691 18.03573,1.0419 31.54513,-11.0605 13.5006,-12.1108 13.1217,-23.2868 2.4568,-34.2738 -10.655,-10.989 -25.74363,1.4016 -34.42573,10.7035 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:21.97px" + id="path34" + sodipodi:nodetypes="ccccc" /> + </g> + <g + transform="matrix(-0.30466673,-0.28350703,0.28350703,-0.30466673,1637.6898,2984.7629)" + id="g35" + style="display:inline"> + <path + d="m 972.67851,1952.1758 c -10.5816,10.0239 -10.9405,27.2982 -1.68601,34.5924 11.26811,8.8718 17.93883,2.1383 32.1602,-9.1189 14.213,-11.2661 14.5155,-22.4445 4.5395,-34.0605 -9.96603,-11.6175 -25.78121,-0.1688 -35.01369,8.587 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:24.68px" + id="path35" + sodipodi:nodetypes="ccccc" /> + </g> + <g + transform="matrix(-0.25623713,-0.16351904,0.16351904,-0.25623713,1866.5089,2766.7631)" + id="g36" + style="display:inline"> + <path + d="m 971.64433,1953.6308 c -9.6145,10.9549 -8.3826,28.189 1.50373,34.6008 12.03655,7.7975 18.05948,0.4789 31.18484,-12.039 13.1162,-12.526 12.389,-23.6848 1.3866,-34.3338 -10.99262,-10.6512 -25.68742,2.2039 -34.07517,11.772 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:33.79px" + id="path36" + sodipodi:nodetypes="ccccc" /> + </g> + <g + transform="matrix(-0.2303235,-0.04979857,0.07477697,-0.20880095,2047.5602,2553.0916)" + id="g37" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:51.9px" + id="path37" + sodipodi:nodetypes="ccccc" /> + </g><g + transform="matrix(-0.16680486,-0.10644747,0.10644747,-0.16680486,1952.9876,2514.3939)" + id="g37-5" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"> + <path + d="m 971.18382,1950.7927 c -6.94927,12.8123 -1.93076,29.3454 9.1302,33.4083 13.46468,4.9378 17.71697,-3.5331 27.74368,-18.6472 10.0161,-15.1201 6.8353,-25.8406 -6.2525,-33.7881 -13.07875,-7.9519 -24.56123,7.8387 -30.62138,19.027 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:51.9px" + id="path37-0" + sodipodi:nodetypes="ccccc" /> + </g> + + <g + transform="matrix(0.272628,-0.772994,0.772994,0.272628,-404.40753,2288.2725)" + id="g39" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:12.53px" + id="path39" /> + </g> + <g + transform="matrix(0.07767912,-0.72974165,0.73737821,0.07687465,-121.44602,2690.1883)" + id="g40" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:15.32px" + id="path40" /> + </g> + <g + transform="matrix(0.122671,-0.658838,0.658838,0.122671,14.642083,2585.917)" + id="g41" + style="display:inline"> + <path + d="m 972.67872,1952.1842 c -10.59429,10.0104 -10.97508,27.2842 -1.72983,34.5902 11.25686,8.8861 17.93611,2.161 32.17171,-9.0781 14.2273,-11.2482 14.5439,-22.4261 4.5827,-34.0548 -9.95136,-11.6301 -25.78103,-0.2014 -35.02458,8.5427 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:15.32px" + id="path41" /> + </g> + <g + transform="matrix(-0.147206,-0.556944,0.556944,-0.147206,516.90158,3065.027)" + id="g42" + style="display:inline"> + <path + d="m 978.01471,1945.6294 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.10999,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.51199,-3.72 -35.86299,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:17.83px" + id="path42" /> + </g> + <g + transform="matrix(-0.208406,-0.386327,0.386327,-0.208406,948.04806,3051.609)" + id="g43" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:23.4px" + id="path43" /> + </g> + <g + transform="matrix(-0.254008,-0.257548,0.257548,-0.254008,1288.1229,3034.6294)" + id="g44" + style="display:inline"> + <path + d="m 978.1985,1927.8822 c -13.96377,4.1786 -22.06998,19.437 -17.09645,30.1195 6.05997,12.9983 15.04962,9.9941 32.81837,6.355 17.76538,-3.6508 23.07368,-13.493 19.40398,-28.3586 -3.6601,-14.8624 -22.93797,-11.7707 -35.1259,-8.1159 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:28.39px" + id="path44" /> + </g> + <g + transform="matrix(-0.14485047,-0.2325948,0.2325948,-0.14485047,1258.2493,2819.1127)" + id="g45" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:37.48px" + id="path45" /> + </g> + <g + transform="matrix(-0.10225592,-0.16762026,0.16762026,-0.10225592,1373.0467,2686.1563)" + id="g46" + style="display:inline"> + <path + d="m 974.40628,1967.8559 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11002,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.51202,-3.72 -35.86302,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:52.3px" + id="path46" /> + </g> + <g + transform="matrix(0.75618993,-0.52802667,0.54004212,0.72246738,-521.96924,1148.6361)" + id="g47" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:10.27px" + id="path47" /> + </g> + <g + transform="matrix(0.72455156,-0.32958515,0.32958515,0.72455156,-109.9906,1016.0965)" + id="g48" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:12.9px" + id="path48" /> + </g> + <g + transform="matrix(0.58226115,-0.03562145,0.03562145,0.58226115,488.4045,1152.648)" + id="g49" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:17.61px" + id="path49" /> + </g> + <g + transform="matrix(0.49063256,0.01838155,-0.01838155,0.49063256,642.21338,1308.8638)" + id="g50" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:20.92px" + id="path50" /> + </g> + <g + transform="matrix(0.27284649,0.09505418,-0.11326108,0.22898598,934.25048,1788.3174)" + id="g51" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:33.54px" + id="path51" /> + </g> + <g + transform="matrix(0.21225646,0.03458965,-0.03929322,0.18684845,813.21596,1931.1164)" + id="g52" + style="display:inline"> + <path + d="m 1002.7955,1955.2194 c -13.75309,4.827 -21.13703,20.4477 -15.66948,30.886 6.66109,12.7008 15.50048,9.2796 33.07958,4.8137 17.5752,-4.4775 22.4175,-14.5571 18.0568,-29.2349 -4.351,-14.675 -23.4632,-10.6854 -35.4669,-6.4648 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:52.1px" + id="path52" /> + </g> + <g + transform="rotate(2.1091452,-1024.0084,926.62863)" + id="g53" + style="display:inline"> + <path + d="m 969.99775,1951.9127 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.62995,-9.2 17.46995,-20.23 9.18995,-33.11 -8.26995,-12.88 -25.51195,-3.72 -35.86295,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:10.27px" + id="path53" /> + </g><g + transform="matrix(0.25153389,0.86224312,-0.90765832,0.23894825,3209.1724,710.5182)" + id="g53-1" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"> + <path + d="m 969.99775,1951.9127 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.62995,-9.2 17.46995,-20.23 9.18995,-33.11 -8.26995,-12.88 -25.51195,-3.72 -35.86295,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:10.27px" + id="path53-0" /> + </g> + <g + transform="matrix(0.783505,0.21102279,-0.21102279,0.783505,517.40994,324.61291)" + id="g54" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:12.66px" + id="path54" /> + </g> + <g + transform="matrix(0.69282964,0.42237233,-0.42237233,0.69282964,954.66742,305.91037)" + id="g55" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:12.66px" + id="path55" /> + </g><g + transform="matrix(0.66554895,-0.27228012,0.24945244,0.72645411,69.114236,1016.7687)" + id="g55-2" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:12.66px" + id="path55-6" /> + </g> + <g + transform="matrix(0.41323218,0.49524386,-0.49524386,0.41323218,1312.0641,776.71136)" + id="g56" + style="display:inline"> + <path + d="m 979.1927,1960.73 c -10.60874,9.9951 -11.01449,27.2684 -1.77981,34.5877 11.24401,8.9023 17.93296,2.1869 32.18471,-9.0317 14.2436,-11.2275 14.5763,-22.405 4.6319,-34.0481 -9.9345,-11.6444 -25.78062,-0.2387 -35.0368,8.4921 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:15.92px" + id="path56" /> + </g><g + transform="matrix(0.41323218,0.49524386,-0.49524386,0.41323218,1310.8114,770.70449)" + id="g56-7" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"> + <path + d="m 1576.6299,1609.8594 c 3.0101,14.2615 17.5463,23.6012 28.6033,19.5276 13.4547,-4.9649 11.2038,-14.1721 9.0458,-32.1808 -2.1698,-18.0063 -11.5397,-24.1099 -26.6577,-21.6815 -15.1141,2.4191 -13.6264,21.8866 -10.9914,34.3347 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:15.92px" + id="path56-1" /> + </g> + <g + transform="matrix(0.34738126,0.54346484,-0.54346484,0.34738126,1403.0162,845.84635)" + id="g57" + style="display:inline"> + <path + d="m 991.46424,1943.5041 c -10.0407,8.6731 -11.37299,24.3365 -3.53214,31.3278 9.54745,8.5039 15.9014,2.6602 29.2662,-6.9862 13.3578,-9.6549 14.2826,-19.788 6.0503,-30.7307 -8.2234,-10.9436 -23.0233,-1.1872 -31.78436,6.3891 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:14.3562px" + id="path57" /> + </g> + <g + transform="matrix(0.24871284,0.40014157,-0.40014157,0.24871284,1184.7806,1168.3324)" + id="g58" + style="display:inline"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:21.8px" + id="path58" /> + </g><g + transform="matrix(0.18538623,0.38676387,-0.37328479,0.19208041,1156.5578,1271.8611)" + id="g58-0" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:21.8px" + id="path58-7" /> + </g><g + transform="matrix(0.18538623,0.38676387,-0.37328479,0.19208041,1600.5582,1540.6118)" + id="g58-0-4" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"> + <path + d="m 989.57702,1980.4468 c 0.57767,14.5594 13.31768,25.4557 24.87688,22.5884 14.067,-3.497 13.3916,-12.9299 14.2787,-31.1369 0.875,-18.2058 -7.3231,-25.3127 -22.6026,-24.5274 -15.27411,0.7766 -17.06452,20.3667 -16.55298,33.0759 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:21.8px" + id="path58-7-8" /> + </g><g + transform="matrix(0.15808771,0.34764656,-0.3183178,0.17265339,1043.8284,1325.7843)" + id="g58-0-8" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:21.8px" + id="path58-7-3" /> + </g><g + transform="matrix(0.37373305,0.07857138,-0.04946275,0.35873228,742.02191,1541.2014)" + id="g58-0-8-1" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:21.8px" + id="path58-7-3-5" /> + </g><g + transform="matrix(0.14200848,0.31865813,-0.28594143,0.15825673,970.02841,1359.2656)" + id="g58-0-8-0" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"> + <path + d="m 978.46073,1946.4854 c -13.15723,5.4288 -19.51922,21.673 -13.62277,32.157 7.18275,12.7561 15.62193,8.9407 32.53124,3.7449 16.9049,-5.2075 21.0858,-15.6923 16.0408,-30.553 -5.0355,-14.8582 -23.46606,-10.095 -34.94927,-5.3489 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:21.8px" + id="path58-7-3-1" /> + </g><g + transform="matrix(0.07332475,0.32153621,-0.27411876,0.10767329,994.39398,1429.7986)" + id="g58-0-8-0-4" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:21.8px" + id="path58-7-3-1-3" /> + </g><g + transform="matrix(-0.00838944,0.29508179,-0.25402681,0.03176577,1022.2859,1578.8246)" + id="g58-0-8-0-4-7" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"> + <path + d="m 974.537,1950.08 c -11.862,8.47 -14.598,25.53 -6.437,34.03 9.938,10.34 17.473,4.59 33.11,-4.6 15.63,-9.2 17.47,-20.23 9.19,-33.11 -8.27,-12.88 -25.512,-3.72 -35.863,3.68 z" + style="fill:#ee7db1;stroke:#ee7db1;stroke-width:21.8px" + id="path58-7-3-1-3-5" /> + </g> + <g + transform="translate(46.5551,251.486)" + id="g59" + style="display:inline" + inkscape:label="Trait noir"> + <path + d="m 1251.32,739.845 c 5.29,-0.046 10.41,0.05 15.35,0.288 495.18,23.94 581.34,498.047 543.52,612.187 -50.28,151.75 -171.06,161.25 -171.06,161.25 0,0 -50.88,106.36 94.73,189.44 91.47,52.19 214.89,-2.44 214.89,-2.44 0,0 121.6388,-71.2248 122.2768,-61.8508 18.17,21.25 -168.2031,204.3949 -367.4631,184.7749 -128.4,-12.64 -217.0937,-114.3441 -217.0937,-114.3441 0,0 -39.24,144.69 24.52,213.36 63.77,68.67 142.25,36.79 142.25,36.79 0,0 30.63,-17.35 36.78,2.45 5.87,18.88 -26.36,30.04 -26.36,30.04 0,0 -116.37,76.39 -303.37,-38.26 -71.21,-43.65 -99.44,-181.84 -99.44,-181.84 0,0 -10.41,-45.65 -10.75,-45.2 l -10.26,45.12 c 0,0 -39.48,98.36 -114.65,169.83 -111.16,105.69 -198.648,156.96 -198.648,156.96 0,0 -41.303,19.33 -49.661,0.61 -5.146,-11.53 24.524,-40.46 24.524,-40.46 0,0 72.347,-61.32 101.775,-138.57 33.36,-87.57 11.04,-210.91 11.04,-210.91 0,0 -44.363,78.67 -170.447,106.07 -276.511,60.09 -355.602,-288.77 -338.435,-310.84 17.167,-22.08 41.691,33.1 41.691,33.1 0,0 106.256,206.69 219.799,165.53 157.613,-57.12 94.725,-189.44 94.725,-189.44 0,0 -120.782,-9.5 -171.057,-161.24 -37.817,-114.15 48.338,-588.252 543.524,-612.192 5.54,-0.268 11.32,-0.356 17.3,-0.264 z" + style="display:none;fill:none;stroke:#080605;stroke-width:16.78px" + id="path59" + sodipodi:nodetypes="ccccsccccccccccccccccccscccccscc" + inkscape:label="Trait noir" /> + </g> + <path + d="m 1307.5688,2084.5039 c 0,0 23.905,-52.8591 33.7691,-80.1063 4.112,-11.3585 7.2136,-23.0598 10.562,-34.6664 3.946,-13.6779 7.9051,-37.7771 11.2264,-41.2053 3.6203,-3.7368 5.198,34.4252 1.6412,56.0869 -2.6271,15.9999 -3.0732,20.7131 -5.5679,30.8955 -3.5278,14.3992 -10.0247,59.7303 -13.258,42.5611 -3.2333,-17.1692 -4.2008,-27.238 -4.2008,-27.238 z" + style="display:inline;fill:#080605;stroke-width:0.748395" + id="path60" + inkscape:label="Trait entre tentacules centre" + sodipodi:nodetypes="caassazcc" /> + <g + transform="matrix(1.2294079,-0.39709979,0.11251605,0.74286756,-463.91561,936.32816)" + id="g61" + style="display:inline" + inkscape:label="trait entre tentacules gauche" + inkscape:transform-center-x="10.270791" + inkscape:transform-center-y="-3.7557593"> + <path + d="m 1039.4921,2003.5954 c 33.0516,-44.4636 35.3598,-69.6565 37.0817,-77.3191 2.3202,-10.3254 6.0201,-36.0203 7.1402,-42.118 3.5062,-18.0743 6.4979,18.1992 6.0268,38.7259 -0.4711,20.5267 -0.8528,33.1589 -1.5744,47.1617 -0.7216,14.0028 -3.2388,66.1474 -7.6439,72.0018 0.8946,-45.5357 -7.2973,-53.0411 -7.2973,-53.0411 0,0 -51.4939,27.3586 -33.7331,14.5888 z" + style="fill:#080605" + id="path61" + sodipodi:nodetypes="csczzccc" /> + </g> + <path + d="m 1646.375,2052.2649 c -43.9202,-37.7053 -49.9786,-57.5128 -59.2451,-78.9657 -5.6366,-13.0444 -11.5002,-34.4859 -13.6767,-33.8974 -2.7465,0.7356 -3.0083,20.2342 3.443,44.5167 6.4451,24.2845 14.1818,42.4921 42.3727,66.2454 18.4822,15.0628 39.2925,29.3071 39.2925,29.3071 0,0 12.4117,-6.0888 -12.1864,-27.2061 z" + style="display:inline;fill:#080605;stroke-width:0.800771" + id="path62" + inkscape:label="trait entre tentacules droite" + sodipodi:nodetypes="scccccs" /><path + d="m 1739.5265,1761.5942 c -42.5235,15.4483 -57.6682,13.592 -73.8155,10.5327 -9.8196,-1.8584 -22.7052,-10.3267 -23.5062,-8.4112 -1.0146,2.4143 12.0697,11.2632 28.2805,17.7686 16.2086,6.5109 41.8857,6.2216 54.0512,-0.4461 6.0507,-1.5088 35.6903,-15.8925 35.6903,-15.8925 0,0 3.1149,-12.204 -20.7003,-3.5515 z" + style="clip-rule:evenodd;display:inline;fill:#080605;fill-rule:evenodd;stroke-width:0.640728;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5" + id="path62-9" + inkscape:label="trait entre tentacules droite" + sodipodi:nodetypes="scccccs" /><path + d="m 867.34353,1759.0438 c 42.11975,16.5174 57.30644,15.0441 73.52587,12.3937 9.86344,-1.6098 22.95882,-9.75 23.71115,-7.8148 0.95333,2.4391 -12.35028,10.9547 -28.72026,17.0486 -16.3679,6.0995 -42.02941,5.1617 -54.02267,-1.8112 -6.01065,-1.6611 -35.27749,-16.7888 -35.27749,-16.7888 0,0 -2.80565,-12.2788 20.7834,-3.0275 z" + style="clip-rule:evenodd;display:inline;fill:#080605;fill-rule:evenodd;stroke-width:0.640728;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5" + id="path62-9-1" + inkscape:label="trait entre tentacules droite" + sodipodi:nodetypes="scccccs" + sodipodi:insensitive="true" /> + <ellipse + style="display:none;fill:none;stroke:#000000;stroke-width:26.7855;stroke-dasharray:none" + id="path64" + cx="1279.2697" + cy="1734.0125" + rx="944.40771" + ry="752.19781" /></g> + <g + transform="matrix(0.55184576,0,0,0.55184576,199.44794,981.66084)" + id="g64" + style="display:inline;stroke-width:0;stroke-dasharray:none" + inkscape:label="Texte"> + <g + transform="matrix(821.689,0,0,821.689,3365.34,2498.52)" + id="g63" + style="display:inline;stroke-width:0;stroke-dasharray:none"> + </g> + <text + x="423.17694" + y="2571.1672" + style="font-size:821.689px;font-family:Baloo-Regular, Baloo, cursive;display:inline;fill:#ffffff;stroke-width:0;stroke-dasharray:none" + id="text63">POULPY</text> + </g> +<g + transform="translate(-9.3303886,-36.784216)" + id="g21" + style="clip-rule:evenodd;display:inline;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5" + inkscape:label="tour noir"> + <path + d="m 1255.1821,724.39679 c 5.29,-0.046 10.41,0.05 15.35,0.288 495.18,23.94 581.34,498.04701 543.52,612.18701 -50.28,151.75 -171.06,161.25 -171.06,161.25 0,0 -12.7207,78.7444 55.0716,148.6216 56.6514,58.3937 127.6365,101.405 209.3016,94.4841 58.5899,-4.9653 130.862,-66.8415 99.5278,-93.7683 -21.3283,-18.3283 -34.9999,19.2513 -51.0811,17.5546 -23.8267,-2.2232 -15.0501,-47.3004 19.4963,-62.6297 62.3457,-26.9164 101.5395,35.3928 86.9032,94.8785 -10.8899,44.2593 -62.7508,109.0407 -165.3601,142.645 -218.5481,61.0039 -356.2894,-110.0748 -356.2894,-110.0748 0,0 -63.2491,72.6099 -34.6291,141.807 5.0825,12.2884 34.0606,83.1622 110.9802,109.5306 71.8346,24.6252 116.5487,-1.5367 118.5063,-25.3422 1.5464,-18.8053 -11.4245,-26.6811 -25.9661,-23.3155 -13.3082,3.0802 -10.7314,17.4612 -21.9548,18.7682 -17.2975,2.0143 -11.4165,-21.1055 -6.6453,-30.2667 7.6796,-14.7457 28.7276,-23.3094 46.0482,-19.164 14.4706,3.4634 31.0113,14.6644 39.4074,31.9041 23.0818,47.3942 -2.4954,104.8379 -80.4398,126.6389 -149.1647,41.7211 -297.1506,-65.2398 -354.0202,-176.9794 -65.9569,-129.5946 -29.6339,-164.711 -29.6339,-164.711 0,0 2.0284,-20.3455 -1.4086,-8.3837 -3.437,11.9618 -62.5989,147.6025 -112.6442,208.8572 -36.1775,44.2806 -74.9884,80.2221 -131.7121,109.9007 -87.90651,45.9938 -158.69562,51.9736 -236.91683,35.8278 -42.69884,-8.8136 -90.26739,-42.0136 -111.01277,-69.1664 -34.84263,-45.6041 -17.68081,-101.8993 17.74403,-119.466 20.75728,-10.2933 37.80301,-8.2333 54.54505,-1.1326 12.80701,5.4318 23.79285,17.2709 28.37942,30.5994 2.09463,6.087 3.47725,16.7194 -0.53604,23.4165 -6.58335,10.9859 -18.36432,11.3465 -22.65504,2.6393 -11.14741,-22.6214 -30.4274,-28.9079 -41.39594,-7.3327 -13.31668,26.194 17.08627,52.2373 44.5459,59.0755 19.99027,4.9781 60.32342,10.5607 109.31696,-8.7342 80.33381,-31.6376 110.99076,-96.7476 127.57416,-161.0739 30.2577,-117.3682 -7.9568,-120.1084 -7.9568,-120.1084 0,0 -75.52242,133.9789 -240.92453,146.2884 -172.75646,12.8568 -296.43122,-99.2234 -327.77517,-166.8476 -40.6785,-87.7634 -4.3468,-140.7103 46.04996,-160.9368 27.91776,-11.2047 58.87134,-5.8098 77.97151,21.1604 6.55969,9.2626 8.89716,27.5748 4.38139,40.5603 -4.51576,12.9854 -15.88474,20.6442 -23.31308,13.0745 -7.78647,-7.9346 -1.07898,-15.456 -11.59743,-31.2695 -7.3052,-10.9827 -38.12569,-12.7752 -47.97654,20.8356 -6.73059,22.9646 7.9355,59.2194 46.23947,85.9592 38.30397,26.7398 107.01031,50.6937 182.62317,42.7638 155.22443,-23.9486 141.65421,-207.1678 141.65421,-207.1678 0,0 -120.782,-9.5 -171.057,-161.24 -37.817,-114.15 48.338,-588.25201 543.52404,-612.19201 5.54,-0.268 11.32,-0.356 17.3,-0.264 z" + style="display:inline;fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:10.5;stroke-dasharray:none;stroke-opacity:1" + id="path21" + sodipodi:nodetypes="ccccsssccsccsssssassssczasassassssssscssssssssscccscc" /> + </g></svg>
@@ -0,0 +1,609 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="150mm" + height="110mm" + viewBox="0 0 150 110" + version="1.1" + id="svg1" + xml:space="preserve" + inkscape:version="1.4 (86a8ad7, 2024-10-11)" + sodipodi:docname="poulpy_arch.svg" + inkscape:export-filename="lib_diagram.png" + inkscape:export-xdpi="150" + inkscape:export-ydpi="150" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#111111" + borderopacity="1" + inkscape:showpageshadow="0" + inkscape:pageopacity="0" + inkscape:pagecheckerboard="1" + inkscape:deskcolor="#d1d1d1" + inkscape:document-units="mm" + inkscape:connector-spacing="0" + inkscape:zoom="1.4702523" + inkscape:cx="387.0084" + inkscape:cy="179.56101" + inkscape:window-width="1781" + inkscape:window-height="1214" + inkscape:window-x="1623" + inkscape:window-y="25" + inkscape:window-maximized="0" + inkscape:current-layer="layer2" /><defs + id="defs1"><marker + style="overflow:visible" + id="ArrowTriangleStylized" + refX="0" + refY="0" + orient="auto-start-reverse" + inkscape:stockid="Stylized triangle arrow" + markerWidth="1" + markerHeight="1" + viewBox="0 0 1 1" + inkscape:isstock="true" + inkscape:collect="always" + preserveAspectRatio="xMidYMid"><path + transform="scale(0.5)" + style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" + d="m 6,0 c -3,1 -7,3 -9,5 0,0 0,-4 2,-5 -2,-1 -2,-5 -2,-5 2,2 6,4 9,5 z" + id="path18" /></marker><inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0.31650688 : 57.520561 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="150.3165 : 57.520561 : 1" + inkscape:persp3d-origin="75.316506 : 39.187228 : 1" + id="perspective3" /><linearGradient + id="linearGradient7" + inkscape:collect="always"><stop + style="stop-color:#1dff98;stop-opacity:1;" + offset="0" + id="stop7" /><stop + style="stop-color:#1dff98;stop-opacity:0;" + offset="1" + id="stop8" /></linearGradient><rect + x="112.05959" + y="307.56271" + width="114.70478" + height="37.273041" + id="rect4" /><marker + style="overflow:visible" + id="Triangle-1" + refX="3.5" + refY="0" + orient="auto-start-reverse" + inkscape:stockid="Triangle arrow" + markerWidth="1" + markerHeight="1" + viewBox="0 0 1 1" + inkscape:isstock="true" + inkscape:collect="always" + preserveAspectRatio="xMidYMid"><path + transform="scale(0.5)" + style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" + d="M 5.77,0 -2.88,5 V -5 Z" + id="path135-2" /></marker><marker + style="overflow:visible" + id="Triangle-1-1-7" + refX="0" + refY="0" + orient="auto-start-reverse" + inkscape:stockid="Triangle arrow" + markerWidth="1" + markerHeight="1" + viewBox="0 0 1 1" + inkscape:isstock="true" + inkscape:collect="always" + preserveAspectRatio="xMidYMid"><path + transform="scale(0.5)" + style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" + d="M 5.77,0 -2.88,5 V -5 Z" + id="path135-2-3-7" /></marker><marker + style="overflow:visible" + id="Triangle-1-1-7-5" + refX="2.5" + refY="0" + orient="auto-start-reverse" + inkscape:stockid="Triangle arrow" + markerWidth="1" + markerHeight="1" + viewBox="0 0 1 1" + inkscape:isstock="true" + inkscape:collect="always" + preserveAspectRatio="xMidYMid"><path + transform="scale(0.5)" + style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" + d="M 5.77,0 -2.88,5 V -5 Z" + id="path135-2-3-7-0" /></marker><marker + style="overflow:visible" + id="Triangle-1-1-8" + refX="3.5" + refY="0" + orient="auto-start-reverse" + inkscape:stockid="Triangle arrow" + markerWidth="1" + markerHeight="1" + viewBox="0 0 1 1" + inkscape:isstock="true" + inkscape:collect="always" + preserveAspectRatio="xMidYMid"><path + transform="scale(0.5)" + style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" + d="M 5.77,0 -2.88,5 V -5 Z" + id="path135-2-3-8" /></marker><marker + style="overflow:visible" + id="Triangle-1-1-8-0" + refX="3.7" + refY="0" + orient="auto-start-reverse" + inkscape:stockid="Triangle arrow" + markerWidth="1" + markerHeight="1" + viewBox="0 0 1 1" + inkscape:isstock="true" + inkscape:collect="always" + preserveAspectRatio="xMidYMid"><path + transform="scale(0.5)" + style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" + d="M 5.77,0 -2.88,5 V -5 Z" + id="path135-2-3-8-1" /></marker><marker + style="overflow:visible" + id="Triangle-1-1-7-4" + refX="0" + refY="0" + orient="auto-start-reverse" + inkscape:stockid="Triangle arrow" + markerWidth="1" + markerHeight="1" + viewBox="0 0 1 1" + inkscape:isstock="true" + inkscape:collect="always" + preserveAspectRatio="xMidYMid"><path + transform="scale(0.5)" + style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" + d="M 5.77,0 -2.88,5 V -5 Z" + id="path135-2-3-7-6" /></marker><marker + style="overflow:visible" + id="Triangle-1-1-7-5-7" + refX="2.5" + refY="0" + orient="auto-start-reverse" + inkscape:stockid="Triangle arrow" + markerWidth="1" + markerHeight="1" + viewBox="0 0 1 1" + inkscape:isstock="true" + inkscape:collect="always" + preserveAspectRatio="xMidYMid"><path + transform="scale(0.5)" + style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" + d="M 5.77,0 -2.88,5 V -5 Z" + id="path135-2-3-7-0-7" /></marker><linearGradient + inkscape:collect="always" + xlink:href="#linearGradient7" + id="linearGradient8" + x1="52.106083" + y1="86.230415" + x2="75.403534" + y2="86.230415" + gradientUnits="userSpaceOnUse" /><linearGradient + inkscape:collect="always" + xlink:href="#linearGradient7" + id="linearGradient9" + x1="53.14452" + y1="66.955605" + x2="76.343185" + y2="66.955605" + gradientUnits="userSpaceOnUse" /><marker + style="overflow:visible" + id="Triangle-1-1-7-3" + refX="0" + refY="0" + orient="auto-start-reverse" + inkscape:stockid="Triangle arrow" + markerWidth="1" + markerHeight="1" + viewBox="0 0 1 1" + inkscape:isstock="true" + inkscape:collect="always" + preserveAspectRatio="xMidYMid"><path + transform="scale(0.5)" + style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" + d="M 5.77,0 -2.88,5 V -5 Z" + id="path135-2-3-7-8" /></marker><marker + style="overflow:visible" + id="Triangle-1-1-7-3-7" + refX="0" + refY="0" + orient="auto-start-reverse" + inkscape:stockid="Triangle arrow" + markerWidth="1" + markerHeight="1" + viewBox="0 0 1 1" + inkscape:isstock="true" + inkscape:collect="always" + preserveAspectRatio="xMidYMid"><path + transform="scale(0.5)" + style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" + d="M 5.77,0 -2.88,5 V -5 Z" + id="path135-2-3-7-8-9" /></marker><marker + style="overflow:visible" + id="Triangle-1-1-7-3-7-6" + refX="0" + refY="0" + orient="auto-start-reverse" + inkscape:stockid="Triangle arrow" + markerWidth="1" + markerHeight="1" + viewBox="0 0 1 1" + inkscape:isstock="true" + inkscape:collect="always" + preserveAspectRatio="xMidYMid"><path + transform="scale(0.5)" + style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" + d="M 5.77,0 -2.88,5 V -5 Z" + id="path135-2-3-7-8-9-5" /></marker></defs><g + inkscape:groupmode="layer" + id="layer3" + inkscape:label="Layer 3"><rect + style="fill:#ffffff;fill-opacity:0.993407;fill-rule:evenodd;stroke:#000000;stroke-width:0.9;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers" + id="rect2" + width="150" + height="110" + x="0" + y="-9.2387199e-07" + ry="0" /></g><g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"><rect + style="fill:#a8aaff;fill-opacity:0.53046596;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:0.2, 0.4;stroke-dashoffset:0;paint-order:stroke fill markers" + id="rect1-5-5" + width="15.565245" + height="19.146524" + x="84.023781" + y="84.629219" + ry="0.93935418" /><rect + style="fill:#fea891;fill-opacity:0.988235;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers" + id="rect1-6" + width="34.694923" + height="37.901432" + x="97.396133" + y="6.9672709" + ry="0" /><text + xml:space="preserve" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;line-height:8.23147px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#303030;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" + x="115.16772" + y="22.360518" + id="text3-3-2"><tspan + sodipodi:role="line" + id="tspan3-6-4" + style="font-size:3.52778px;line-height:8.23147px;fill:#303030;fill-opacity:1;stroke:none;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" + x="115.16772" + y="22.360518">PPML, PSI, STATS...</tspan></text><text + xml:space="preserve" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;line-height:8.23147px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#303030;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" + x="116.22602" + y="31.666801" + id="text3-3-2-0"><tspan + sodipodi:role="line" + id="tspan3-6-4-9" + style="font-size:3.52778px;line-height:8.23147px;fill:#303030;fill-opacity:1;stroke:none;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" + x="116.65289" + y="31.666801">PIR, FHE-RAM, TMHE... </tspan></text><text + xml:space="preserve" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;line-height:8.23147px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#303030;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" + x="115.69688" + y="40.44392" + id="text3-3-2-0-1"><tspan + sodipodi:role="line" + id="tspan3-6-4-9-3" + style="font-size:3.52778px;line-height:8.23147px;fill:#303030;fill-opacity:1;stroke:none;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" + x="115.69688" + y="40.44392">LATTICE ARITHMETIC</tspan></text><text + xml:space="preserve" + style="font-size:3.52778px;line-height:3.78648px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;inline-size:36.8671;display:inline;fill:#2d2d2d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" + x="63.255154" + y="111.84376" + id="text1-1-1" + transform="translate(28.534147,-13.244033)"><tspan + x="63.255154" + y="111.84376" + id="tspan4">spqlios- +</tspan><tspan + x="63.255154" + y="115.63023" + id="tspan12">arithmetic</tspan></text><text + xml:space="preserve" + style="font-size:4.23333px;line-height:4.23333px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;inline-size:35.1767;display:inline;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers" + x="63.255154" + y="111.84376" + id="text1-8-9-6" + transform="translate(51.948454,-100.41339)"><tspan + x="63.255154" + y="111.84376" + id="tspan13">Backend Agnostic<tspan + y="111.84376" + id="tspan14"> </tspan></tspan><tspan + x="63.255154" + y="116.07709" + id="tspan15">Applications</tspan></text><image + width="8.6626749" + height="8.6626749" + preserveAspectRatio="none" + xlink:href="data:image/png;base64,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 " + id="image1" + x="87.495079" + y="85.822609" /><image + width="8.6626749" + height="8.6626749" + preserveAspectRatio="none" + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABHNCSVQICAgIfAhkiAAAIABJREFU eJzs3Xd0VNUWBvBvWnojhfRAAiR0Uui9SC8iWJ4iiCKIFSv6bAgq2BUbiiKKSLMhRZAoRRGQXgMJ LQnpFdLr3PcHxodA2px7585Mvt9aLNfCmXM3cDJ3z7ln7wMQERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER EREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREA aNQOgIgsQ8yMGU4OGWUbJMk4SO1YiKyNBtJuNzeM2PTNNwVqx9JQTACIiDd/IhlYWxLABICoiePN n0g+1pQEMAEgasJ48yeSn7UkAUwAiJoo3vyJlGMNSQATAKImiDd/IuVZehLABICoieHNn8h8LDkJ YAJA1ITw5k9kfpaaBDABIGoiePMnUo8lJgFMAIiaAN78idRnaUkAEwAiG8ebP5HlsKQkgAkAkQ3j zZ/I8lhKEsAEgMhG8eZPZLksIQlgAkBkg3jzJ7J8aicBTACIbAxv/kTWQ80kgAkAkQ3hzZ/I+qiV BDABILIRvPkTWS81kgAmAEQ2gDd/Iutn7iSACQCRlePNn8h2mDMJYAJAZMV48yeyPeZKApgAEFkp 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 AADF+h93sVD41VvktwAAAABJRU5ErkJggg== " + id="image1-1" + x="103.15053" + y="94.472801" /><image + width="8.6626749" + height="8.6626749" + preserveAspectRatio="none" + xlink:href="data:image/png;base64,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 " + id="image1-7" + x="119.37961" + y="96.347794" /><image + width="8.6626749" + height="8.6626749" + preserveAspectRatio="none" + xlink:href="data:image/png;base64,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 " + id="image1-5" + x="135.48961" + y="86.034958" /><path + style="fill:none;fill-opacity:0.52967;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#Triangle-1-1-7);paint-order:stroke fill markers" + d="m 96.466692,90.6767 c 6.661618,-0.0661 8.588368,-0.23235 12.909578,-7.75109" + id="path3-3" + sodipodi:nodetypes="cc" /><path + style="fill:none;fill-opacity:0.52967;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#Triangle-1-1-7-4);paint-order:stroke fill markers" + d="m 135.00972,90.54507 c -6.66162,-0.0661 -8.58837,-0.23235 -12.90955,-7.75109" + id="path3-3-7" + sodipodi:nodetypes="cc" /><path + style="fill:none;fill-opacity:0.52967;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#Triangle-1-1-7-5);paint-order:stroke fill markers" + d="m 108.27595,95.2827 c 4.9493,-4.92296 4.81575,-7.77515 5.41596,-12.46727" + id="path3-3-0" + sodipodi:nodetypes="cc" /><path + style="fill:none;fill-opacity:0.52967;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#Triangle-1-1-7-5-7);paint-order:stroke fill markers" + d="m 123.28455,95.28978 c -4.9493,-4.92296 -4.81575,-7.77515 -5.41596,-12.46727" + id="path3-3-0-3" + sodipodi:nodetypes="cc" /><text + xml:space="preserve" + transform="scale(0.26458333)" + id="text4" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;line-height:31.1111px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect4);display:inline;fill:none;fill-opacity:0.995604;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" /><text + xml:space="preserve" + style="font-size:3.52777px;line-height:3.78648px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#ffffff;fill-opacity:0.993407;fill-rule:evenodd;stroke:#000000;stroke-width:0.899999;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers" + x="122.01137" + y="30.592821" + id="text2"><tspan + sodipodi:role="line" + id="tspan2" + style="stroke-width:0.9" + x="122.01137" + y="30.592821"></tspan></text><path + style="display:inline;fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.765;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-start:url(#ArrowTriangleStylized)" + d="m 114.75405,50.175384 0.10588,12.066228" + id="path14" + inkscape:connector-type="polyline" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cc" /><rect + style="fill:#ffefa8;fill-opacity:0.46594983;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:0.2, 0.4;stroke-dashoffset:0;paint-order:stroke fill markers" + id="rect1-5-5-5" + width="68.46389" + height="76.710312" + x="7.3420134" + y="7.1017885" + ry="0.93935418" /></g><g + inkscape:groupmode="layer" + id="layer2" + inkscape:label="Layer 2"><rect + style="fill:#9affd2;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:0.2, 0.4;stroke-dashoffset:0;paint-order:stroke fill markers" + id="rect1-5-3" + width="36.559639" + height="15.809674" + x="96.366692" + y="64.703583" + ry="0" /><rect + style="fill:#9affd2;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:0.2, 0.4;stroke-dashoffset:0;paint-order:stroke fill markers" + id="rect1-5" + width="36.604614" + height="15.877158" + x="35.467823" + y="64.703583" + ry="0" /><rect + style="fill:#91e6fe;fill-opacity:0.988235;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers" + id="rect1-2" + width="34.694912" + height="8.3827906" + x="97.396133" + y="65.948601" + ry="0" /><text + xml:space="preserve" + style="font-size:5.29167px;line-height:12.3472px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;inline-size:37.1169;display:inline;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers" + x="63.255154" + y="111.84376" + id="text1-7" + transform="translate(51.780571,-40.099091)"><tspan + x="63.255154" + y="111.84376" + id="tspan16">poulpy-backend</tspan></text><rect + style="fill:#9affd2;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:0.2, 0.4;stroke-dashoffset:0;paint-order:stroke fill markers" + id="rect1-5-1" + width="36.582123" + height="15.832165" + x="35.467823" + y="42.850578" + ry="0" /><path + style="fill:none;fill-opacity:0.52967;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#Triangle-1-1-7-3-7-6);paint-order:stroke fill markers" + d="M 71.267012,70.135069 C 84.642814,70.453755 87.78271,38.841542 100.40133,39.075514" + id="path3-3-4-1-2" + sodipodi:nodetypes="cc" /><rect + style="fill:#9affd2;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:0.2, 0.4;stroke-dashoffset:0;paint-order:stroke fill markers" + id="rect1-5-1-3" + width="36.559628" + height="15.832165" + x="35.467823" + y="21.683891" + ry="0" /><rect + style="fill:#91e6fe;fill-opacity:0.988235;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers" + id="rect1" + width="34.326412" + height="8.3926239" + x="36.744686" + y="23.014816" + ry="0" /><rect + style="fill:#92e6ff;fill-opacity:0.969231;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers" + id="rect1-20" + width="34.326412" + height="8.3926239" + x="36.735874" + y="66.14859" + ry="0" /><path + style="fill:none;fill-opacity:0.52967;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#Triangle-1-1-7-3);paint-order:stroke fill markers" + d="M 71.071099,27.21113 C 83.247577,27.103519 90.064952,21.143842 100.38621,21.202453" + id="path3-3-4" + sodipodi:nodetypes="cc" /><path + style="fill:none;fill-opacity:0.52967;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#Triangle-1-1-7-3-7);paint-order:stroke fill markers" + d="M 71.071099,48.303762 C 83.516552,48.195865 88.565504,30.341774 100.36621,30.417908" + id="path3-3-4-1" + sodipodi:nodetypes="cc" /><path + style="display:inline;fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.264583, 0.264583;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#Triangle-1)" + d="m 72.072433,72.642162 24.294259,-0.02249" + id="path2-3" + inkscape:connector-type="polyline" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cc" /><rect + style="fill:#91e6fe;fill-opacity:0.988235;fill-rule:evenodd;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers" + id="rect1-20-7" + width="34.326412" + height="8.3926239" + x="36.744686" + y="44.107449" + ry="0" /><text + xml:space="preserve" + style="font-size:5.29167px;line-height:12.3472px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;inline-size:23.1195;display:inline;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers" + x="63.255154" + y="111.84376" + id="text1" + transform="translate(-9.6712543,-39.845796)"><tspan + x="63.255154" + y="111.84376" + id="tspan17">poulpy-hal</tspan></text><text + xml:space="preserve" + style="font-size:4.23333px;line-height:9.87776px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;inline-size:36.8671;display:inline;fill:#2d2d2d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" + x="63.255154" + y="111.84376" + id="text1-1" + transform="translate(-9.5719303,-32.862898)"><tspan + x="63.255154" + y="111.84376" + id="tspan18">ZNX API</tspan></text><text + xml:space="preserve" + style="font-size:4.23333px;line-height:9.87776px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;inline-size:36.8671;display:inline;fill:#2d2d2d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" + x="63.255154" + y="111.84376" + id="text1-1-0" + transform="translate(51.679143,-32.882948)" + inkscape:transform-center-x="0.6362468" + inkscape:transform-center-y="1.1452442"><tspan + x="63.255154" + y="111.84376" + id="tspan19">ZNX API</tspan></text><text + xml:space="preserve" + style="font-size:4.23333px;line-height:9.87776px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;inline-size:36.8671;display:inline;fill:#2d2d2d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" + x="63.255154" + y="111.84376" + id="text1-1-8" + transform="translate(-10.163182,-54.843723)"><tspan + x="63.255154" + y="111.84376" + id="tspan20">RLWE API</tspan></text><text + xml:space="preserve" + style="font-size:4.23333px;line-height:9.87776px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;inline-size:36.8671;display:inline;fill:#2d2d2d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" + x="63.255154" + y="111.84376" + id="text1-1-8-4" + transform="translate(-9.6181503,-75.873115)"><tspan + x="63.255154" + y="111.84376" + id="tspan21">SCHEME API</tspan></text><text + xml:space="preserve" + style="font-size:5.29167px;line-height:12.3472px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;inline-size:23.1195;display:inline;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers" + x="63.255154" + y="111.84376" + id="text1-8" + transform="translate(-10.03551,-62.138412)"><tspan + x="63.255154" + y="111.84376" + id="tspan22">poulpy-core</tspan></text><text + xml:space="preserve" + style="font-size:5.29167px;line-height:12.3472px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;inline-size:35.1767;display:inline;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers" + x="63.255154" + y="111.84376" + id="text1-8-9" + transform="translate(-9.3743876,-83.103301)"><tspan + x="63.255154" + y="111.84376" + id="tspan23">poulpy-schemes</tspan></text><path + style="display:inline;fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.264583, 0.264583;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#Triangle-1-1-8)" + d="M 53.646411,64.703584 V 58.682749" + id="path2-3-7-9" + inkscape:connector-type="polyline" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cc" /><path + style="display:inline;fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.264583, 0.264583;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#Triangle-1-1-8-0)" + d="M 53.646411,42.85058 V 37.516058" + id="path2-3-7-9-5" + inkscape:connector-type="polyline" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cc" /><text + xml:space="preserve" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;line-height:8.23147px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#303030;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" + x="21.40082" + y="49.034626" + id="text3"><tspan + sodipodi:role="line" + id="tspan3" + style="font-size:3.52778px;line-height:8.23147px;fill:#303030;fill-opacity:1;stroke:none;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" + x="21.40082" + y="49.034626">LWE, GLWE, GGSW...</tspan></text><text + xml:space="preserve" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;line-height:8.23147px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#303030;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" + x="22.140085" + y="71.407509" + id="text3-6"><tspan + sodipodi:role="line" + id="tspan3-0" + style="font-size:3.52778px;line-height:8.23147px;fill:#303030;fill-opacity:1;stroke:none;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" + x="22.140085" + y="71.407509">VecZnx, VmpPMat...</tspan></text><text + xml:space="preserve" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;line-height:8.23147px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#303030;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" + x="21.606615" + y="28.183018" + id="text3-3"><tspan + sodipodi:role="line" + id="tspan3-6" + style="font-size:3.52778px;line-height:8.23147px;fill:#303030;fill-opacity:1;stroke:none;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" + x="21.606615" + y="28.183018">TFHE, CKKS, BGV, ...</tspan></text><text + xml:space="preserve" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93889px;line-height:11.5241px;font-family:Akrobat;-inkscape-font-specification:Akrobat;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#303030;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;paint-order:stroke fill markers" + x="42.594212" + y="15.101881" + id="text3-3-1"><tspan + sodipodi:role="line" + id="tspan3-6-1" + style="font-size:4.93889px;line-height:11.5241px;fill:#303030;fill-opacity:1;stroke:#000000;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1" + x="42.594212" + y="15.101881">Backend Agostic Crates</tspan></text></g></svg>
@@ -10,7 +10,7 @@ homepage = "https://github.com/phantomzone-org/poulpy"
documentation = "https://docs.rs/poulpy" [dependencies] -poulpy-hal = "0.1.2" +poulpy-hal = {path="../poulpy-hal"} rug = {workspace = true} criterion = {workspace = true} itertools = {workspace = true}
@@ -1,15 +1,38 @@
+# 🐙 Poulpy-Backend +**Poulpy-Backend** is a Rust crate that provides concrete implementations of **`poulpy-hal`**. This crate is used to instantiate projects implemented with **`poulpy-hal`**, **`poulpy-core`** and/or **`poulpy-schemes`**. + +## Backends + +### cpu-spqlios -## spqlios-arithmetic +This module provides a CPU AVX2 accelerated backend through C bindings over [**spqlios-arithmetic**](https://github.com/tfhe/spqlios-arithmetic). + +- Currently supported: `FFT64` backend +- Planned: `NTT120` backend + +### Build Notes + +This backend is built and compiled automatically and has been tested on wsl/ubuntu. + +- `cmake` is invoked automatically by the build script (`build.rs`) when compiling the crate. +- No manual setup is required beyond having a standard Rust toolchain. +- Build options can be changed in `/build/cpu_spqlios.rs` +- Automatic build of cpu-spqlios/spqlios-arithmetic can be disabled in `build.rs`. + +Spqlios-arithmetic is windows/mac compatible but building for those platforms is slightly different (see [spqlios-arithmetic/wiki/build](https://github.com/tfhe/spqlios-arithmetic/wiki/build)) and has not been tested in Poulpy. + +### Example -### WSL/Ubuntu -To use this crate you need to build spqlios-arithmetic, which is provided a as a git submodule: -1) Initialize the sub-module -2) $ cd backend/spqlios-arithmetic -3) mdkir build -4) cd build -5) cmake .. -6) make +```rust +use poulpy_backend::cpu_spqlios::FFT64; +use poulpy_hal::{api::ModuleNew, layouts::Module}; -### Others -Steps 3 to 6 might change depending of your platform. See [spqlios-arithmetic/wiki/build](https://github.com/tfhe/spqlios-arithmetic/wiki/build) for additional information and build options.+let log_n: usize = 10; +let module = Module<FFT64> = Module<FFT64>::new(1<<log_n); +``` + +## Contributors + +To add a backend, implement the open extension traits from **`poulpy-hal/oep`** for a struct that implements the `Backend` trait. +This will automatically make your backend compatible with the API of **`poulpy-hal`**, **`poulpy-core`** and **`poulpy-schemes`**.
@@ -1,14 +0,0 @@
-# Use the Google style in this project. -BasedOnStyle: Google - -# Some folks prefer to write "int& foo" while others prefer "int &foo". The -# Google Style Guide only asks for consistency within a project, we chose -# "int& foo" for this project: -DerivePointerAlignment: false -PointerAlignment: Left - -# The Google Style Guide only asks for consistency w.r.t. "east const" vs. -# "const west" alignment of cv-qualifiers. In this project we use "east const". -QualifierAlignment: Left - -ColumnLimit: 120
@@ -1,20 +0,0 @@
-name: Auto-Release - -on: - workflow_dispatch: - push: - branches: [ "main" ] - -jobs: - build: - name: Auto-Release - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 3 -# sparse-checkout: manifest.yaml scripts/auto-release.sh - - - run: - ${{github.workspace}}/scripts/auto-release.sh
@@ -1,6 +0,0 @@
-cmake-build-* -.idea - -build -.vscode -.*.sh
@@ -1,69 +0,0 @@
-cmake_minimum_required(VERSION 3.8) -project(spqlios) - -# read the current version from the manifest file -file(READ "manifest.yaml" manifest) -string(REGEX MATCH "version: +(([0-9]+)\\.([0-9]+)\\.([0-9]+))" SPQLIOS_VERSION_BLAH ${manifest}) -#message(STATUS "Version: ${SPQLIOS_VERSION_BLAH}") -set(SPQLIOS_VERSION ${CMAKE_MATCH_1}) -set(SPQLIOS_VERSION_MAJOR ${CMAKE_MATCH_2}) -set(SPQLIOS_VERSION_MINOR ${CMAKE_MATCH_3}) -set(SPQLIOS_VERSION_PATCH ${CMAKE_MATCH_4}) -message(STATUS "Compiling spqlios-fft version: ${SPQLIOS_VERSION_MAJOR}.${SPQLIOS_VERSION_MINOR}.${SPQLIOS_VERSION_PATCH}") - -#set(ENABLE_SPQLIOS_F128 ON CACHE BOOL "Enable float128 via libquadmath") -set(WARNING_PARANOID ON CACHE BOOL "Treat all warnings as errors") -set(ENABLE_TESTING ON CACHE BOOL "Compiles unittests and integration tests") -set(DEVMODE_INSTALL OFF CACHE BOOL "Install private headers and testlib (mainly for CI)") - -if (NOT CMAKE_BUILD_TYPE OR CMAKE_BUILD_TYPE STREQUAL "") - set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type: Release or Debug" FORCE) -endif() -message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") - -if (WARNING_PARANOID) - add_compile_options(-Wall -Werror -Wno-unused-command-line-argument) -endif() - -message(STATUS "CMAKE_HOST_SYSTEM_NAME: ${CMAKE_HOST_SYSTEM_NAME}") -message(STATUS "CMAKE_SYSTEM_PROCESSOR: ${CMAKE_SYSTEM_PROCESSOR}") -message(STATUS "CMAKE_SYSTEM_NAME: ${CMAKE_SYSTEM_NAME}") - -if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)") - set(X86 ON) - set(AARCH64 OFF) -else () - set(X86 OFF) - # set(ENABLE_SPQLIOS_F128 OFF) # float128 are only supported for x86 targets -endif () -if (CMAKE_SYSTEM_PROCESSOR MATCHES "(aarch64)|(arm64)") - set(AARCH64 ON) -endif () - -if (CMAKE_SYSTEM_NAME MATCHES "(Windows)|(MSYS)") - set(WIN32 ON) -endif () -if (WIN32) - #overrides for win32 - set(X86 OFF) - set(AARCH64 OFF) - set(X86_WIN32 ON) -else() - set(X86_WIN32 OFF) - set(WIN32 OFF) -endif (WIN32) - -message(STATUS "--> WIN32: ${WIN32}") -message(STATUS "--> X86_WIN32: ${X86_WIN32}") -message(STATUS "--> X86_LINUX: ${X86}") -message(STATUS "--> AARCH64: ${AARCH64}") - -# compiles the main library in spqlios -add_subdirectory(spqlios) - -# compiles and activates unittests and itests -if (${ENABLE_TESTING}) - enable_testing() - add_subdirectory(test) -endif() -
@@ -1,77 +0,0 @@
-# Contributing to SPQlios-fft - -The spqlios-fft team encourages contributions. -We encourage users to fix bugs, improve the documentation, write tests and to enhance the code, or ask for new features. -We encourage researchers to contribute with implementations of their FFT or NTT algorithms. -In the following we are trying to give some guidance on how to contribute effectively. - -## Communication ## - -Communication in the spqlios-fft project happens mainly on [GitHub](https://github.com/tfhe/spqlios-fft/issues). - -All communications are public, so please make sure to maintain professional behaviour in -all published comments. See [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) for -guidelines. - -## Reporting Bugs or Requesting features ## - -Bug should be filed at [https://github.com/tfhe/spqlios-fft/issues](https://github.com/tfhe/spqlios-fft/issues). - -Features can also be requested there, in this case, please ensure that the features you request are self-contained, -easy to define, and generic enough to be used in different use-cases. Please provide an example of use-cases if -possible. - -## Setting up topic branches and generating pull requests - -This section applies to people that already have write access to the repository. Specific instructions for pull-requests -from public forks will be given later. - -To implement some changes, please follow these steps: - -- Create a "topic branch". Usually, the branch name should be `username/small-title` - or better `username/issuenumber-small-title` where `issuenumber` is the number of - the github issue number that is tackled. -- Push any needed commits to your branch. Make sure it compiles in `CMAKE_BUILD_TYPE=Debug` and `=Release`, with `-DWARNING_PARANOID=ON`. -- When the branch is nearly ready for review, please open a pull request, and add the label `check-on-arm` -- Do as many commits as necessary until all CI checks pass and all PR comments have been resolved. - - > _During the process, you may optionnally use `git rebase -i` to clean up your commit history. If you elect to do so, - please at the very least make sure that nobody else is working or has forked from your branch: the conflicts it would generate - and the human hours to fix them are not worth it. `Git merge` remains the preferred option._ - -- Finally, when all reviews are positive and all CI checks pass, you may merge your branch via the github webpage. - -### Keep your pull requests limited to a single issue - -Pull requests should be as small/atomic as possible. - -### Coding Conventions - -* Please make sure that your code is formatted according to the `.clang-format` file and - that all files end with a newline character. -* Please make sure that all the functions declared in the public api have relevant doxygen comments. - Preferably, functions in the private apis should also contain a brief doxygen description. - -### Versions and History - -* **Stable API** The project uses semantic versioning on the functions that are listed as `stable` in the documentation. A version has - the form `x.y.z` - * a patch release that increments `z` does not modify the stable API. - * a minor release that increments `y` adds a new feature to the stable API. - * In the unlikely case where we need to change or remove a feature, we will trigger a major release that - increments `x`. - - > _If any, we will mark those features as deprecated at least six months before the major release._ - -* **Experimental API** Features that are not part of the stable section in the documentation are experimental features: you may test them at - your own risk, - but keep in mind that semantic versioning does not apply to them. - -> _If you have a use-case that uses an experimental feature, we encourage -> you to tell us about it, so that this feature reaches to the stable section faster!_ - -* **Version history** The current version is reported in `manifest.yaml`, any change of version comes up with a tag on the main branch, and the history between releases is summarized in `Changelog.md`. It is the main source of truth for anyone who wishes to - get insight about - the history of the repository (not the commit graph). - -> Note: _The commit graph of git is for git's internal use only. Its main purpose is to reduce potential merge conflicts to a minimum, even in scenario where multiple features are developped in parallel: it may therefore be non-linear. If, as humans, we like to see a linear history, please read `Changelog.md` instead!_
@@ -1,18 +0,0 @@
-# Changelog - -All notable changes to this project will be documented in this file. -this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [2.0.0] - 2024-08-21 - -- Initial release of the `vec_znx` (except convolution products), `vec_rnx` and `zn` apis. -- Hardware acceleration available: AVX2 (most parts) -- APIs are documented in the wiki and are in "beta mode": during the 2.x -> 3.x transition, functions whose API is satisfactory in test projects will pass in "stable mode". - -## [1.0.0] - 2023-07-18 - -- Initial release of the double precision fft on the reim and cplx backends -- Coeffs-space conversions cplx <-> znx32 and tnx32 -- FFT-space conversions cplx <-> reim4 layouts -- FFT-space multiplications on the cplx, reim and reim4 layouts. -- In this first release, the only platform supported is linux x86_64 (generic C code, and avx2/fma). It compiles on arm64, but without any acceleration.
@@ -1,201 +0,0 @@
- Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License.
@@ -1,65 +0,0 @@
-# SPQlios library - - - -The SPQlios library provides fast arithmetic for Fully Homomorphic Encryption, and other lattice constructions that arise in post quantum cryptography. - -<img src="docs/api-full.svg"> - -Namely, it is divided into 4 sections: - -* The low-level DFT section support FFT over 64-bit floats, as well as NTT modulo one fixed 120-bit modulus. It is an upgrade of the original spqlios-fft module embedded in the TFHE library since 2016. The DFT section exposes the traditional DFT, inverse-DFT, and coefficient-wise multiplications in DFT space. -* The VEC_ZNX section exposes fast algebra over vectors of small integer polynomial modulo $X^N+1$. It proposed in particular efficient (prepared) vector-matrix products, scalar-vector products, convolution products, and element-wise products, operations that naturally occurs on gadget-decomposed Ring-LWE coordinates. -* The RNX section is a simpler variant of VEC_ZNX, to represent single polynomials modulo $X^N+1$ (over the reals or over the torus) when the coefficient precision fits on 64-bit doubles. The small vector-matrix API of the RNX section is particularly adapted to reproducing the fastest CGGI-based bootstrappings. -* The ZN section focuses over vector and matrix algebra over scalars (used by scalar LWE, or scalar key-switches, but also on non-ring schemes like Frodo, FrodoPIR, and SimplePIR). - -### A high value target for hardware accelerations - -SPQlios is more than a library, it is also a good target for hardware developers. -On one hand, the arithmetic operations that are defined in the library have a clear standalone mathematical definition. And at the same time, the amount of work in each operations is sufficiently large so that meaningful functions only require a few of these. - -This makes the SPQlios API a high value target for hardware acceleration, that targets FHE. - -### SPQLios is not an FHE library, but a huge enabler - -SPQlios itself is not an FHE library: there is no ciphertext, plaintext or key. It is a mathematical library that exposes efficient algebra over polynomials. Using the functions exposed, it is possible to quickly build efficient FHE libraries, with support for the main schemes based on Ring-LWE: BFV, BGV, CGGI, DM, CKKS. - - -## Dependencies - -The SPQLIOS-FFT library is a C library that can be compiled with a standard C compiler, and depends only on libc and libm. The API -interface can be used in a regular C code, and any other language via classical foreign APIs. - -The unittests and integration tests are in an optional part of the code, and are written in C++. These tests rely on -[```benchmark```](https://github.com/google/benchmark), and [```gtest```](https://github.com/google/googletest) libraries, and therefore require a C++17 compiler. - -Currently, the project has been tested with the gcc,g++ >= 11.3.0 compiler under Linux (x86_64). In the future, we plan to -extend the compatibility to other compilers, platforms and operating systems. - - -## Installation - -The library uses a classical ```cmake``` build mechanism: use ```cmake``` to create a ```build``` folder in the top level directory and run ```make``` from inside it. This assumes that the standard tool ```cmake``` is already installed on the system, and an up-to-date c++ compiler (i.e. g++ >=11.3.0) as well. - -It will compile the shared library in optimized mode, and ```make install``` install it to the desired prefix folder (by default ```/usr/local/lib```). - -If you want to choose additional compile options (i.e. other installation folder, debug mode, tests), you need to run cmake manually and pass the desired options: -``` -mkdir build -cd build -cmake ../src -CMAKE_INSTALL_PREFIX=/usr/ -make -``` -The available options are the following: - -| Variable Name | values | -| -------------------- | ------------------------------------------------------------ | -| CMAKE_INSTALL_PREFIX | */usr/local* installation folder (libs go in lib/ and headers in include/) | -| WARNING_PARANOID | All warnings are shown and treated as errors. Off by default | -| ENABLE_TESTING | Compiles unit tests and integration tests | - ------- - -<img src="docs/logo-sandboxaq-black.svg"> - -<img src="docs/logo-inpher1.png">
@@ -1,416 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - width="200.88994mm" - height="172.38969mm" - viewBox="0 0 200.88994 172.38969" - version="1.1" - id="svg1" - xml:space="preserve" - inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" - sodipodi:docname="api-full.svg" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview - id="namedview1" - pagecolor="#ffffff" - bordercolor="#000000" - borderopacity="0.25" - inkscape:showpageshadow="2" - inkscape:pageopacity="0.0" - inkscape:pagecheckerboard="0" - inkscape:deskcolor="#d1d1d1" - inkscape:document-units="mm" - inkscape:zoom="0.62240079" - inkscape:cx="367.93012" - inkscape:cy="346.23992" - inkscape:window-width="1072" - inkscape:window-height="683" - inkscape:window-x="56" - inkscape:window-y="32" - inkscape:window-maximized="1" - inkscape:current-layer="layer1" /><defs - id="defs1"><marker - style="overflow:visible" - id="RoundedArrow" - refX="0" - refY="0" - orient="auto-start-reverse" - inkscape:stockid="Rounded arrow" - markerWidth="1" - markerHeight="1" - viewBox="0 0 1 1" - inkscape:isstock="true" - inkscape:collect="always" - preserveAspectRatio="xMidYMid"><path - transform="scale(0.7)" - d="m -0.21114562,-4.1055728 6.42229122,3.21114561 a 1,1 90 0 1 0,1.78885438 L -0.21114562,4.1055728 A 1.236068,1.236068 31.717474 0 1 -2,3 v -6 a 1.236068,1.236068 148.28253 0 1 1.78885438,-1.1055728 z" - style="fill:context-stroke;fill-rule:evenodd;stroke:none" - id="path8" /></marker><marker - style="overflow:visible" - id="ConcaveTriangle" - refX="0" - refY="0" - orient="auto-start-reverse" - inkscape:stockid="Concave triangle arrow" - markerWidth="1" - markerHeight="1" - viewBox="0 0 1 1" - inkscape:isstock="true" - inkscape:collect="always" - preserveAspectRatio="xMidYMid"><path - transform="scale(0.7)" - d="M -2,-4 9,0 -2,4 c 2,-2.33 2,-5.66 0,-8 z" - style="fill:context-stroke;fill-rule:evenodd;stroke:none" - id="path7" /></marker></defs><g - inkscape:label="Layer 1" - inkscape:groupmode="layer" - id="layer1" - transform="translate(-4.4292935,-3.5395608)"><rect - style="fill:#ffffff;stroke:none;stroke-width:1.01556;stroke-dasharray:1.42178, 0.710893;stroke-dashoffset:0" - id="rect9" - width="203.19905" - height="174.81824" - x="3.2396183" - y="2.4070516" - rx="3" - ry="3" /><path - style="fill:#000000;stroke-width:0.141927" - d="m 47.127979,35.729714 c -2.191188,-0.177706 -3.753709,-0.609 -5.651502,-1.559917 -2.219299,-1.112026 -4.13594,-2.693687 -5.452762,-4.499784 -0.187893,-0.257693 -0.836686,-1.270382 -1.162921,-1.815182 -0.654751,-1.093375 -1.38865,-3.077478 -1.700287,-4.596741 -0.446686,-2.177613 -0.364498,-4.471764 0.247192,-6.900115 0.381473,-1.514388 0.875683,-2.711363 1.643355,-3.980183 0.639507,-1.056981 1.269028,-1.875068 2.106989,-2.7381173 1.005054,-1.0351484 1.950799,-1.8215221 2.947798,-2.4510868 1.169569,-0.7385264 1.782702,-1.0292324 3.423019,-1.6229531 0.676084,-0.2447109 2.185118,-0.5805031 2.998292,-0.6671812 0.714137,-0.07609 2.942802,-0.065782 3.661279,0.017035 2.874915,0.3311971 5.32212,1.3454049 7.754385,3.2136807 0.786197,0.6038967 1.707201,1.4321514 2.009941,1.8075181 0.108267,0.1342446 0.276237,0.3285756 0.373247,0.4318436 0.281527,0.29966 0.479827,0.569916 0.913574,1.24504 0.883114,1.374577 1.412511,2.458114 1.866371,3.81999 0.557817,1.67381 0.782264,3.102192 0.782544,4.980023 1.4e-4,1.406673 -0.1179,2.303785 -0.501777,3.81149 -0.551077,2.164343 -1.369011,3.763224 -3.035242,5.933148 -0.827677,1.077885 -2.215238,2.326558 -3.535999,3.182061 -0.40393,0.261633 -1.390944,0.806171 -1.888641,1.041963 -1.448751,0.686367 -3.164802,1.163568 -4.785683,1.330809 -0.625037,0.06447 -2.308808,0.07382 -3.013172,0.01669 z m 2.140448,-0.634273 c 1.609061,-0.111052 3.135462,-0.444652 4.543713,-0.993033 0.938984,-0.365644 2.360058,-1.146908 3.262562,-1.793648 1.204984,-0.863523 2.637945,-2.256601 2.806585,-2.728473 0.0613,-0.171706 0.0653,-0.570075 0.008,-0.763113 -0.0244,-0.08142 -0.13361,-0.248083 -0.24269,-0.370326 -0.296347,-0.332132 -0.293497,-0.322941 -0.296137,-0.954822 -0.003,-0.625311 -0.0216,-0.692718 -0.25607,-0.911073 -0.291317,-0.271252 -0.282857,-0.584131 0.0309,-1.143284 0.1193,-0.212572 0.22508,-0.446992 0.23507,-0.520933 0.0222,-0.164368 -0.15427,-0.565646 -0.41554,-0.9449 -0.224947,-0.326524 -0.458227,-0.786603 -0.516007,-1.017644 -0.0438,-0.175016 -0.0369,-0.457598 0.0249,-1.023871 l 0.025,-0.229372 -0.277037,-0.02691 c -0.455987,-0.04429 -0.614227,-0.09192 -0.823037,-0.247717 -0.371157,-0.27693 -0.559684,-0.282931 -1.055534,-0.03363 l -0.300967,0.151315 -0.446177,-0.0079 c -0.25961,-0.0044 -0.466377,0.0087 -0.494487,0.03066 -0.0266,0.02114 -0.19327,0.04971 -0.37044,0.06351 l -0.322127,0.02516 -0.15556,-0.22178 C 54.0694,21.200136 53.73405,20.822909 53.237903,20.314091 l -0.298327,-0.305951 0.0276,-0.625345 0.0276,-0.625347 0.10622,-0.07854 c 0.130907,-0.09679 0.195277,-0.244231 0.179347,-0.41079 -0.0111,-0.115996 -0.0236,-0.127449 -0.15018,-0.137985 -0.132957,-0.01109 -0.141017,-0.02009 -0.217547,-0.244083 l -0.0795,-0.232595 0.16123,-0.140762 c 0.0887,-0.0774 0.423137,-0.32054 0.743264,-0.540276 0.817957,-0.561451 0.751027,-0.476739 0.751027,-0.950647 0,-0.419361 0.0465,-0.564981 0.1805,-0.565182 0.0349,-5.2e-5 0.156527,-0.05591 0.270117,-0.12419 0.318177,-0.191169 0.322367,-0.192069 0.901244,-0.193527 l 0.544497,-0.0017 0.21704,-0.122994 c 0.237087,-0.134359 0.372157,-0.158243 1.334171,-0.235975 0.29553,-0.02385 0.548997,-0.0318 0.563277,-0.01756 0.0142,0.01424 0.0316,0.209698 0.0386,0.434405 0.0106,0.341226 0.023,0.411682 0.0751,0.427607 0.0343,0.01048 0.332797,0.08861 0.663257,0.173627 0.330447,0.085 0.688487,0.196866 0.795634,0.248565 0.28331,0.136674 0.34968,0.103871 0.34968,-0.172841 v -0.217673 l 0.174217,-0.02385 c 0.466187,-0.06386 0.610017,-0.02769 0.982767,0.247638 0.109677,0.08098 0.256997,0.171347 0.327367,0.200753 0.15658,0.06543 0.693627,0.07373 0.693627,0.01066 0,-0.08985 -0.23808,-0.810261 -0.32381,-0.979834 -0.0494,-0.09773 -0.13618,-0.206387 -0.192817,-0.241453 -0.33375,-0.206675 -0.47944,-0.441219 -0.518167,-0.834233 -0.0267,-0.270893 -0.0277,-0.272832 -0.12504,-0.248922 -0.25498,0.06264 -0.36441,0.208781 -0.412947,0.551563 -0.0362,0.255221 -0.0934,0.406248 -0.15415,0.406248 -0.019,0 -0.13566,-0.117568 -0.25933,-0.261267 -0.236267,-0.274509 -0.303807,-0.433111 -0.304157,-0.714156 -1.5e-4,-0.161781 -0.0118,-0.178956 -0.310017,-0.459982 -0.41144,-0.387702 -0.557467,-0.458585 -0.874767,-0.424602 -0.275697,0.02953 -0.271767,0.0159 -0.12193,0.423195 0.0698,0.189736 0.0839,0.202648 0.3984,0.365748 0.179327,0.09299 0.374647,0.208213 0.434047,0.256051 0.10627,0.08561 0.10702,0.08886 0.0469,0.205041 -0.0335,0.06491 -0.0611,0.20181 -0.0611,0.304161 v 0.186094 l -0.17567,0.05984 c -0.288597,0.09833 -0.412327,0.08072 -0.569737,-0.08098 l -0.137047,-0.140816 0.273807,-0.01109 c 0.25116,-0.01013 0.276707,-0.0187 0.308737,-0.102945 0.0536,-0.141103 0.0126,-0.194472 -0.277107,-0.360245 -0.535557,-0.306431 -0.736427,-0.479088 -0.849944,-0.730544 l -0.0563,-0.124784 -0.22531,0.188923 -0.22531,0.188924 -0.269507,-0.02297 c -0.261057,-0.02228 -0.271647,-0.01992 -0.337957,0.07679 -0.0376,0.05486 -0.13748,0.178003 -0.22183,0.273566 -0.20543,0.232744 -0.376867,0.490263 -0.488017,0.733035 -0.0906,0.197922 -0.0938,0.200429 -0.288397,0.223798 -0.34319,0.04123 -0.872637,0.02638 -1.058984,-0.0297 -0.25069,-0.07548 -0.34666,-0.178843 -0.39872,-0.429608 -0.0466,-0.224856 -0.0276,-0.585485 0.0413,-0.780723 0.0824,-0.233557 0.504707,-0.319143 1.097194,-0.222357 0.20115,0.03285 0.379617,0.05975 0.396577,0.05975 0.0314,0 0.13942,-0.36946 0.11482,-0.392567 -0.007,-0.007 -0.0768,-0.03888 -0.15425,-0.07094 -0.159377,-0.06604 -0.164827,-0.09412 -0.0886,-0.456481 l 0.0474,-0.225309 0.370757,-0.06508 c 0.41009,-0.07198 0.40957,-0.07164 0.68023,-0.481919 0.246977,-0.374362 0.489177,-0.465819 1.124821,-0.424725 l 0.39073,0.02525 -0.003,-0.127564 c -0.003,-0.178187 -0.0542,-0.516294 -0.081,-0.543035 -0.0122,-0.01223 -0.14408,0.02167 -0.29298,0.07539 -0.311597,0.112404 -0.348907,0.09963 -0.418237,-0.143156 -0.0603,-0.211211 -0.0917,-0.244275 -0.275967,-0.290672 -0.17435,-0.04394 -0.24335,-0.140124 -0.24335,-0.3393384 0,-0.15508 0.16754,-0.3059507 0.521127,-0.4692787 l 0.2798,-0.129239 -0.27333,-0.2518927 C 57.481609,8.549631 55.822818,7.4780726 54.731104,6.9315699 52.759836,5.9447765 50.713178,5.4911375 48.236063,5.4919405 c -1.313284,4.28e-4 -1.572851,0.03224 -3.285762,0.403137 -0.740967,0.1604174 -2.025878,0.6083704 -2.96657,1.0342121 -0.835908,0.3784057 -1.899004,1.0085554 -1.866584,1.1064154 0.0079,0.02306 0.122338,0.05312 0.254906,0.06692 0.132577,0.0138 0.317089,0.05862 0.41003,0.09969 0.09294,0.04106 0.270369,0.09469 0.394289,0.119202 0.265442,0.0525 0.394297,0.125151 0.394297,0.222294 0,0.03861 0.02962,0.08194 0.06569,0.09627 0.113332,0.0449 0.948174,0.0719 0.948174,0.03066 0,-0.1137234 -0.09508,-0.2834454 -0.176038,-0.3142224 -0.05041,-0.01922 -0.242693,-0.03486 -0.427232,-0.03486 -0.205452,0 -0.335521,-0.01503 -0.335521,-0.0387 0,-0.02132 0.02533,-0.04848 0.05635,-0.06037 0.07111,-0.02726 0.07277,-0.1644987 0.0035,-0.2598777 -0.04595,-0.0629 -0.100018,-0.07286 -0.394219,-0.07286 -0.187534,0 -0.340999,-0.01171 -0.341042,-0.02603 -1.4e-4,-0.05023 0.546416,-0.4549767 0.732186,-0.5422217 0.158051,-0.07426 0.247813,-0.08823 0.567391,-0.08855 l 0.379635,-4.19e-4 0.12731,0.21313 c 0.17146,0.2870547 0.27177,0.3538057 0.567327,0.3775237 0.283587,0.0228 0.472977,-0.04709 0.595957,-0.219771 0.0769,-0.1079497 0.15837,-0.1290907 0.325277,-0.08439 0.12859,0.03442 0.10317,0.1089797 -0.11265,0.3302537 -0.222507,0.228121 -0.365837,0.283918 -0.730054,0.28425 -0.12786,1.14e-4 -0.24279,0.01686 -0.25538,0.0373 -0.0126,0.02036 -0.0114,0.138675 0.003,0.262865 l 0.0255,0.2257894 h 0.462007 c 0.41414,0 0.46427,-0.007 0.483757,-0.06849 0.0552,-0.173968 0.16254,-0.231931 0.42945,-0.231931 h 0.255407 l 0.0435,-0.145174 c 0.0705,-0.235372 0.061,-0.256749 -0.12721,-0.284966 -0.145367,-0.02175 -0.167657,-0.03634 -0.150197,-0.09814 0.0113,-0.03992 0.0287,-0.102158 0.0389,-0.138352 0.0384,-0.1374257 0.158767,-0.1660347 0.702114,-0.1667767 0.28779,-4.28e-4 0.532037,0.0079 0.542757,0.01878 0.0373,0.0373 -0.10074,0.2022107 -0.272667,0.3256847 -0.16907,0.121412 -0.17255,0.128016 -0.15407,0.292007 0.0259,0.229441 0.10916,0.299529 0.393317,0.330909 l 0.2347,0.02595 V 8.1595363 7.7999983 l 0.272247,-0.132314 c 0.33172,-0.1612047 0.820597,-0.2642017 1.123674,-0.2367177 0.16406,0.01485 0.34198,0.07792 0.704937,0.2498907 0.267017,0.126497 0.549517,0.252164 0.627787,0.279254 0.160167,0.05547 0.190787,0.121395 0.196627,0.423474 l 0.004,0.193397 0.18788,0.04508 c 0.10333,0.02481 0.19696,0.05975 0.20807,0.07775 0.0229,0.03695 -0.0753,0.245541 -0.16221,0.344686 -0.0455,0.05189 -0.11839,0.06569 -0.34708,0.06569 H 48.60409 l -0.10112,0.2253094 c -0.0556,0.123919 -0.11284,0.225422 -0.12717,0.225562 -0.136207,8.74e-4 -1.357941,-0.285482 -1.347651,-0.316337 0.007,-0.02184 0.23579,-0.1578954 0.507817,-0.3023704 0.27202,-0.144466 0.494587,-0.282126 0.494587,-0.3059047 0,-0.06674 -0.286427,-0.309899 -0.398017,-0.337907 -0.1329,-0.03337 -0.830777,0.05329 -0.957654,0.118914 -0.0554,0.02865 -0.15747,0.120399 -0.22683,0.203879 -0.10929,0.1315627 -0.15494,0.1564777 -0.342557,0.1870097 -0.11906,0.01939 -0.23312,0.04551 -0.25348,0.05809 -0.066,0.0408 -0.0403,0.201818 0.0391,0.244311 0.10413,0.05574 0.0684,0.09513 -0.13184,0.1455674 -0.0933,0.0235 -0.169617,0.06229 -0.169617,0.08614 0,0.08264 -0.12473,0.180782 -0.22981,0.180782 -0.12179,0 -0.183257,-0.08774 -0.183257,-0.261567 0,-0.1532024 -0.0625,-0.1713654 -0.32657,-0.09492 -0.320737,0.09284 -0.697737,0.2774664 -1.098764,0.5380784 -0.195307,0.1269253 -0.412787,0.2498133 -0.483277,0.2730773 -0.12668,0.04185 -0.12791,0.04429 -0.10469,0.2162656 0.0622,0.460788 0.28245,0.64564 0.870127,0.730292 l 0.335817,0.0484 0.0188,0.41114 c 0.0176,0.385073 0.0239,0.411866 0.0994,0.422629 0.0444,0.0061 0.13848,-0.01267 0.209157,-0.04219 0.10715,-0.04473 0.14216,-0.09113 0.21061,-0.27886 0.10985,-0.301278 0.22201,-0.404318 0.545997,-0.501646 0.481087,-0.144509 0.436837,-0.09081 0.437077,-0.530455 1.2e-4,-0.212144 0.0177,-0.448556 0.039,-0.5253626 0.056,-0.201634 0.20343,-0.3600963 0.362857,-0.3900083 0.1348,-0.02533 0.505777,0.03957 0.905397,0.158287 l 0.199687,0.05932 -0.0266,0.3398719 -0.0266,0.339872 0.17304,-0.01948 c 0.0952,-0.01075 0.325687,-0.09204 0.512247,-0.180694 0.18655,-0.08866 0.347127,-0.153255 0.356827,-0.143558 0.01,0.0097 0.029,0.204124 0.0428,0.432037 l 0.0252,0.414399 0.27636,0.162916 0.276357,0.162925 v 0.231591 c 0,0.269225 -0.0342,0.31432 -0.319187,0.420017 -0.1547,0.05739 -0.18776,0.0857 -0.18776,0.160924 0,0.06028 0.0319,0.10657 0.0939,0.136368 0.577567,0.277525 0.543497,0.250522 0.555947,0.440451 0.0112,0.170954 0.009,0.174474 -0.0999,0.174474 -0.0612,0 -0.218477,-0.03355 -0.349407,-0.0746 -0.13093,-0.04106 -0.26785,-0.07487 -0.30427,-0.07513 -0.0736,-5.68e-4 -0.219147,-0.07391 -0.261777,-0.131922 -0.06,-0.08177 -0.0242,-0.218216 0.084,-0.319187 0.158907,-0.148432 0.148817,-0.278012 -0.0289,-0.371627 -0.0943,-0.04962 -0.15882,-0.02787 -0.15882,0.05373 0,0.07775 -0.18186,0.159056 -0.454997,0.203373 -0.13183,0.0214 -0.25033,0.03905 -0.26332,0.03914 -0.013,1.4e-4 -0.0449,-0.0511 -0.0709,-0.113873 -0.0873,-0.210659 -0.28787,-0.276423 -0.557047,-0.182581 -0.11095,0.0387 -0.11826,0.05259 -0.11826,0.225309 0,0.127326 0.0153,0.184084 0.0495,0.184084 0.0273,0 0.13916,0.02664 0.248717,0.05914 l 0.1992,0.05914 -0.12296,0.126855 -0.12295,0.126864 -0.162887,-0.09546 -0.16289,-0.09546 -0.13022,0.08046 c -0.147277,0.09102 -0.447337,0.410381 -0.555057,0.590752 -0.0512,0.08579 -0.183777,0.179935 -0.463587,0.329354 -0.347817,0.185743 -0.448357,0.265232 -0.894264,0.707037 -0.481227,0.47679 -0.504237,0.506589 -0.545857,0.707114 -0.0505,0.24306 -0.12343,0.323266 -0.509847,0.560395 -0.465997,0.285954 -0.712637,0.506615 -0.845657,0.756551 -0.06543,0.12288 -0.07111,0.184442 -0.04962,0.535104 0.01337,0.217944 0.01241,0.396254 -0.0017,0.396254 -0.01441,0 -0.151438,-0.06875 -0.304457,-0.152687 l -0.278205,-0.152687 -0.01109,-0.250993 -0.01109,-0.250993 -0.769812,-0.02752 c -0.649946,-0.02332 -0.80196,-0.01826 -0.976339,0.03206 -0.244012,0.0705 -0.264367,0.07801 -0.337967,0.124408 -0.03101,0.01957 -0.150032,0.07626 -0.264569,0.126078 -0.294725,0.128148 -0.458497,0.286793 -0.57887,0.560726 -0.05696,0.129642 -0.144379,0.321222 -0.194261,0.425748 -0.144301,0.302342 -0.152714,0.514749 -0.03224,0.814803 0.056,0.139591 0.115017,0.261922 0.131074,0.271846 0.01607,0.01 0.159213,0.03739 0.318137,0.06098 0.377909,0.05617 0.469357,0.02621 0.939945,-0.308153 0.206535,-0.146755 0.436902,-0.298359 0.511935,-0.336902 l 0.136421,-0.07006 0.02315,0.115865 c 0.04455,0.222906 0.0221,0.336342 -0.117647,0.593573 -0.101616,0.187063 -0.129458,0.27071 -0.100009,0.300508 0.02245,0.02271 0.207208,0.07478 0.410598,0.115673 0.203391,0.04088 0.375279,0.07985 0.381989,0.08657 0.007,0.007 0.0332,0.306345 0.05879,0.665873 l 0.04656,0.653686 0.187762,0.04656 c 0.103259,0.0256 0.354208,0.07137 0.557659,0.101827 0.294839,0.04412 0.390931,0.04604 0.473501,0.0098 0.0896,-0.0394 0.564782,-0.54957 0.564782,-0.60645 0,-0.01127 0.0356,-0.0615 0.0792,-0.111663 0.0643,-0.07399 0.10501,-0.08776 0.21592,-0.07329 0.718184,0.09384 0.907174,0.16717 0.994714,0.385962 0.0316,0.07897 0.0852,0.15426 0.119127,0.167277 0.0775,0.0297 0.721997,0.03023 0.832337,5.67e-4 0.0462,-0.01241 0.099,-0.07032 0.12058,-0.132298 l 0.0384,-0.110274 0.198617,0.100237 c 0.23145,0.116809 0.399747,0.300281 0.619127,0.674949 0.10229,0.17471 0.188,0.277375 0.24408,0.292392 0.0475,0.01276 0.266037,0.02341 0.485547,0.02368 l 0.399107,5.67e-4 0.13768,0.199914 c 0.16666,0.241977 0.24679,0.492228 0.24753,0.773011 2.9e-4,0.115472 0.0217,0.25184 0.0477,0.303033 0.0564,0.111549 0.29559,0.254006 0.477517,0.28446 0.0723,0.01214 0.22262,0.0663 0.33407,0.120503 0.189867,0.0923 0.246437,0.09983 0.898274,0.119421 l 0.695647,0.02088 0.104747,0.202937 c 0.14949,0.289588 0.27241,0.401286 0.564447,0.512879 0.14121,0.05399 0.25644,0.117752 0.25609,0.141759 -2.9e-4,0.02402 -0.0327,0.167092 -0.0719,0.317962 -0.0574,0.220862 -0.10327,0.30866 -0.23561,0.450619 -0.254427,0.27292 -0.401577,0.510047 -0.439637,0.70845 -0.0191,0.09974 -0.054,0.434814 -0.0775,0.744618 -0.0629,0.829392 -0.0821,0.89451 -0.314887,1.066425 -0.10614,0.07845 -0.21615,0.170491 -0.24447,0.204614 -0.0425,0.05119 -0.128077,0.06203 -0.490147,0.06203 h -0.438657 l -0.11771,0.12204 c -0.203897,0.211384 -0.246077,0.444975 -0.144557,0.800545 0.0291,0.101853 0.06,0.282303 0.0688,0.400999 0.0132,0.178877 0.004,0.227073 -0.0571,0.281638 -0.17434,0.157125 -0.853417,1.066302 -0.986127,1.320273 -0.14026,0.268386 -0.15393,0.281857 -0.39109,0.385255 -0.135147,0.05897 -0.287987,0.132053 -0.339617,0.162489 -0.26992,0.159108 -0.405447,0.205208 -0.605607,0.205994 -0.23821,8.73e-4 -0.307837,0.0518 -0.288947,0.210938 0.01,0.08098 0.0429,0.112616 0.161387,0.153448 0.0821,0.0283 0.16406,0.06622 0.1821,0.08422 0.054,0.05399 -0.009,0.184477 -0.18451,0.382058 -0.0911,0.102595 -0.165657,0.197039 -0.165657,0.20989 0,0.01284 0.0431,0.07985 0.0958,0.148956 0.088,0.115332 0.1125,0.125588 0.30041,0.125588 h 0.204634 v 0.233635 c 0,0.134865 -0.0342,0.342012 -0.0809,0.489964 l -0.0809,0.25633 0.0883,0.06185 c 0.0486,0.03407 0.190604,0.140229 0.315594,0.236002 0.19877,0.152293 0.22876,0.192016 0.23921,0.3168 0.006,0.07845 -0.005,0.159982 -0.0266,0.181149 -0.0411,0.04106 -0.757404,0.04289 -0.886434,0.0026 -0.0734,-0.02315 -0.262287,-0.177174 -0.680884,-0.555292 -0.11294,-0.102019 -0.28299,-0.313245 -0.37788,-0.469392 -0.0949,-0.156146 -0.254617,-0.416058 -0.354967,-0.577577 -0.16707,-0.268936 -0.18244,-0.313306 -0.18244,-0.526708 0,-0.128165 0.0175,-0.372318 0.0388,-0.542546 0.0496,-0.395382 0.0162,-0.51744 -0.231197,-0.845781 -0.24395,-0.323728 -0.26084,-0.442965 -0.1437,-1.014104 0.0478,-0.233066 0.0759,-0.452409 0.0625,-0.48744 -0.0137,-0.03564 -0.0646,-0.06368 -0.11548,-0.06368 -0.0501,0 -0.10513,-0.02953 -0.12236,-0.06569 -0.0172,-0.03617 -0.0369,-0.623355 -0.0438,-1.304916 l -0.0124,-1.239205 -0.119877,-0.125186 c -0.17938,-0.187307 -0.755857,-0.56119 -0.937127,-0.607779 -0.351822,-0.09042 -0.344642,-0.08491 -0.373472,-0.285298 -0.0444,-0.30824 -0.11389,-0.420584 -0.656709,-1.061777 l -0.521442,-0.615527 0.06482,-0.454689 c 0.03564,-0.250084 0.06587,-0.533863 0.06709,-0.630623 0.0035,-0.274964 0.05102,-0.348486 0.359266,-0.556515 0.37962,-0.256174 0.442694,-0.358681 0.415726,-0.675745 -0.01144,-0.134245 -0.04289,-0.320129 -0.07006,-0.413062 l -0.04927,-0.168988 -0.185892,-0.0044 c -0.102237,-0.0026 -0.276904,-0.02341 -0.388156,-0.04691 -0.111244,-0.02359 -0.281219,-0.04281 -0.377725,-0.04281 -0.199171,0 -0.244511,-0.02927 -0.395145,-0.25509 -0.05914,-0.08867 -0.125963,-0.173155 -0.148485,-0.187762 -0.02254,-0.01459 -0.132105,-0.133154 -0.243505,-0.263468 -0.272562,-0.318826 -0.535505,-0.441706 -1.176986,-0.55005 l -0.140824,-0.02358 v -0.199949 -0.199949 l -0.177619,-0.01992 c -0.133005,-0.01502 -0.214511,-8.73e-4 -0.324515,0.05495 -0.101669,0.05189 -0.182843,0.06753 -0.263624,0.05093 -0.06421,-0.01319 -0.175855,-0.03276 -0.248145,-0.04333 -0.07225,-0.01066 -0.165223,-0.03363 -0.206535,-0.05119 -0.04132,-0.01756 -0.207872,-0.07085 -0.370143,-0.118442 -0.22309,-0.06543 -0.302622,-0.106517 -0.326148,-0.168385 -0.01712,-0.04499 -0.0449,-0.08782 -0.06167,-0.09515 -0.01686,-0.007 -0.06438,-0.08011 -0.105697,-0.161781 -0.04132,-0.08168 -0.116432,-0.19787 -0.166961,-0.258261 l -0.09186,-0.109812 0.06674,-0.173583 c 0.113882,-0.296211 0.114467,-0.331705 0.0079,-0.463967 -0.173749,-0.215027 -0.583998,-0.830573 -0.661259,-0.992162 -0.04202,-0.0879 -0.08904,-0.298542 -0.107688,-0.482539 -0.03704,-0.365311 -0.07976,-0.435252 -0.271662,-0.444407 l -0.12675,-0.0061 0.0061,0.2835 c 0.0087,0.367277 0.100341,0.623965 0.372387,1.038609 0.18812,0.286741 0.218844,0.359223 0.233582,0.551135 l 0.01695,0.220975 -0.210834,-0.01747 c -0.253194,-0.02097 -0.311332,-0.06674 -0.553588,-0.435461 -0.300674,-0.457644 -0.346355,-0.617502 -0.352872,-1.235073 -0.0035,-0.341192 -0.02132,-0.551458 -0.04918,-0.585119 -0.02394,-0.02892 -0.132603,-0.100079 -0.241392,-0.158198 -0.325562,-0.173916 -0.329223,-0.179717 -0.329223,-0.521667 0,-0.268632 -0.0061,-0.296325 -0.05617,-0.246469 -0.138709,0.13871 -0.57417,1.390014 -0.783195,2.250575 -0.09397,0.386856 -0.236107,1.342203 -0.332412,2.234319 -0.02752,0.254636 -0.0484,0.883415 -0.04805,1.445732 5.07e-4,0.848095 0.01354,1.081109 0.08801,1.577173 0.267014,1.777706 0.739717,3.356617 1.397831,4.669054 1.318822,2.630021 3.343825,4.786857 5.978491,6.367706 0.939988,0.564011 2.857197,1.322169 3.819321,1.510359 0.21686,0.04237 0.723797,0.142833 1.126534,0.223151 0.402747,0.08028 0.918151,0.160899 1.145341,0.179052 0.586063,0.04682 1.52304,0.0491 2.159217,0.0052 z" - id="path23" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.4, 0.7;stroke-dashoffset:0;marker-end:url(#ConcaveTriangle)" - d="M 50.144569,72.668694 V 52.396365" - id="path24" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="M 54.377905,47.986365 V 68.258694" - id="path25" - sodipodi:nodetypes="cc" /><rect - style="fill:#d7f4ee;stroke-width:0.3;stroke-dasharray:0.600001, 0.600001" - id="rect19" - width="137.32346" - height="14.956152" - x="19.258987" - y="103.4627" - rx="5" - ry="5" /><rect - style="fill:#d5f6ff;fill-opacity:1;stroke-width:0.3;stroke-dasharray:0.600001, 0.600001" - id="rect21" - width="75.022408" - height="24.181534" - x="19.258987" - y="77.004356" - rx="5" - ry="5" /><rect - style="fill:#d5f6ff;fill-opacity:1;stroke-width:0.3;stroke-dasharray:0.600001, 0.600001" - id="rect22" - width="29.907101" - height="24.181534" - x="96.517372" - y="77.004356" - rx="5" - ry="5" /><rect - style="fill:#d5f6ff;stroke-width:0.430238;stroke-dasharray:0.860478, 0.860478" - id="rect23" - width="29.096771" - height="24.733751" - x="128.26723" - y="77.004356" - rx="5" - ry="5" /><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#000000;stroke-width:0.3" - x="21.87299" - y="81.671806" - id="text8"><tspan - sodipodi:role="line" - id="tspan8" - style="font-size:3.52778px;fill:#000000;stroke-width:0.3" - x="21.87299" - y="81.671806">VEC_ZNX arithmetic API</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#000000;stroke-width:0.3" - x="23.751926" - y="108.33081" - id="text17"><tspan - sodipodi:role="line" - id="tspan17" - style="font-size:3.52778px;fill:#000000;stroke-width:0.3" - x="23.751926" - y="108.33081">FFT/NTT API</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#000000;stroke-width:0.3" - x="102.77969" - y="81.375206" - id="text18"><tspan - sodipodi:role="line" - id="tspan18" - style="font-size:3.52778px;fill:#000000;stroke-width:0.3" - x="102.77969" - y="81.375206">RNX API</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#000000;stroke-width:0.3" - x="136.22255" - y="81.576088" - id="text19"><tspan - sodipodi:role="line" - id="tspan19" - style="font-size:3.52778px;fill:#000000;stroke-width:0.3" - x="136.22255" - y="81.576088">ZN API</tspan></text><text - xml:space="preserve" - style="font-size:5.64444px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#d5f6ff;fill-opacity:1;stroke-width:0.3;stroke-dasharray:0.600001, 0.600001" - x="29.593384" - y="21.761526" - id="text24"><tspan - sodipodi:role="line" - id="tspan24" - style="stroke-width:0.3" - x="29.593384" - y="21.761526" /></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="53.504406" - y="40.35569" - id="text25"><tspan - sodipodi:role="line" - id="tspan25" - style="font-size:3.52778px;text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="53.504406" - y="40.35569">FHE libraries</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="53.504406" - y="46.352917" - id="tspan26">BGV, BFV, CKKS</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#666666;stroke-width:0.3" - x="115.64725" - y="40.35569" - id="text31"><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="115.64725" - y="40.35569" - id="tspan31">FHE libraries</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="115.64725" - y="46.352917" - id="tspan54">TFHE/CGGI, DM</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#666666;stroke-width:0.3" - x="181.2632" - y="40.35569" - id="text35"><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="181.2632" - y="40.35569" - id="tspan35">Other PQC:</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="181.2632" - y="46.352917" - id="tspan36">NTRU,</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="181.2632" - y="52.350143" - id="tspan37">Falcon</tspan></text><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.4, 0.7;stroke-dashoffset:0;marker-end:url(#ConcaveTriangle)" - d="M 88.77376,72.668694 V 52.396365" - id="path37" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="M 93.007096,47.986365 V 68.258694" - id="path38" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.4, 0.7;stroke-dashoffset:0;marker-end:url(#ConcaveTriangle)" - d="M 118.40721,72.668694 V 52.396365" - id="path39" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="M 122.64057,47.986365 V 68.258694" - id="path40" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="m 150.15741,72.668694 7.9375,-20.272329" - id="path41" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="m 163.38661,47.986365 -7.9375,20.272329" - id="path42" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="M 159.77083,98.887499 180.38924,58.727986" - id="path43" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="M 182.6127,62.177205 161.99429,102.33673" - id="path44" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.4, 0.7;stroke-dashoffset:0;marker-end:url(#ConcaveTriangle)" - d="M 21.569551,72.668694 V 52.396365" - id="path45" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.7;stroke-dasharray:1.39999998, 0.69999999;marker-end:url(#ConcaveTriangle);stroke-dashoffset:0" - d="M 25.802887,47.986365 V 68.258694" - id="path46" - sodipodi:nodetypes="cc" /><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="24.922491" - y="40.35569" - id="text47"><tspan - sodipodi:role="line" - id="tspan46" - style="font-size:3.52778px;text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="24.922491" - y="40.35569">Onion-PIRs</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="24.922491" - y="46.352917" - id="tspan47">Onion, spiral</tspan></text><image - width="28.970715" - height="28.970715" - preserveAspectRatio="none" - xlink:href="data:image/png;base64,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 " - id="image1" - x="12.163744" - y="145.64278" /><image - width="33.19001" - height="33.19001" - preserveAspectRatio="none" - xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABHNCSVQICAgIfAhkiAAAIABJREFU eJzs3Xd0VNUWBvBvWnrvHRIgoZNC70V6EcHyLCCKKFb02SsWBLFjQ1FEEWk2pENUQOmEDoGEloT0 SnqfeX9gfAikzbl37szk+63Fci2cOXcDJ3P3nHv2PgARERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER EREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREQGASukA iMg8xIy/38EW5esBwxClYyGyPIY9tTUYtW/T90VKR9JUTACIiDd/IklYVhLABICohePNn0hKlpME MAEgasF48yeSg2UkAUwAiFoo3vyJ5GT+SQATAKIWiDd/IlMw7ySACQBRC8ObP5EpmW8SwASAqAXh zZ9ICeaZBDABIGohePMnUpL5JQFMAIhaAN78icyBeSUBTACIrBxv/kTmxHySACYARFaMN38ic2Qe SQATACIrxZs/kTlTPglgAkBkhXjzJ7IEyiYBTACIrAxv/kSWRLkkgAkAkRXhzZ/IEimTBDABILIS vPkTWTLTJwFMAIisAG/+RNbAtEkAEwAiC8ebP5E1MV0SwASAyILx5k9kjUyTBDABILJQvPkTWTP5 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 " - id="image1-3" - x="55.115635" - y="142.72488" /><image - width="30.466211" - height="30.466211" - preserveAspectRatio="none" - xlink:href="data:image/png;base64,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 AElFTkSuQmCC " - id="image1-2" - x="101.64658" - y="144.70558" /><image - width="28.021778" - height="26.974236" - preserveAspectRatio="none" - xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAhcAAAIDCAYAAAC3njzFAAAABHNCSVQICAgIfAhkiAAAIABJREFU eJzcvWmXHFeSpveYXfeIXIBEAuBSXIq1jGakI43Okf6Izqh/kX6fvo2OpJ4uksVibV01XSSQe0a4 XzN9MLvuHpGRAIhidU3LecDM9PBwv34Xs9deW678w3/6T95hFBw3Q0QZTKiiOELvAyd2j6tyo0do FY5HR9TZ9DBIoXpBqax8Q+cjVAdZM9CjKMVG3EfoYIuyKWtcFakDvW05ZkBN8KqYKENn0CsjUKtQ /IjOjH68o+iI4xhg9Gw74X7lOLAeCqsB1mYUqwiVoSj36zVDOWL0Dq/Oyo2u3tPLHUVqPFN67soa o0cHR31DkWtUneI9TmF0xQDocDG8VE7qwLO7LdWU1+sjalmzGpUOx2vFqGgRNp1xX0BV6cfC0Sgc D+DqXPYVY+R0hCLKVpxBOgZZ4QKu94g67gVxoXPox4HeN1QR7vpjRu3AtxSr9CYUh1GdCnhREMG8 YCYohZ7C2pxua2gdcamMK6ilsB1hoGOoHWW75WhzhdeBW+8YqtDrCvEBLRtwUD9BXGEcKVoQLTBW eq1Y3VDVKKsVGwOkR1yQcaCMAyhse8XLCuUIhkpn93Tjlh6gK9z0cC9GFaVIYVXXrCr0mw2IY8dH jFR0uKeXEdji2jHKOVt6xjJSZGBdb8BGnFOqrqnaIWKs6h3Yhooi3RqRjoLR2T2bamyPnnOE8PL6 kv+A8z8Vp95c85fSsT1aoT7ydBz5tCrDyRP+z7LmaxHuq0NRRnGqACrETC2ggoiCgiEgggsUYF0r BcNVMFWqKIKgInSilFHoFMBxNao6VQ097jk+O+Enn3zEZ599yk9/+jlnz58hEs8tpVD6QilKdaPW inlFBJBoA+6oK+K896EO4uASf7sIyPIKz38gIvtfx0VxjxuoKEUVMccHx4aR4X7D3dUNV68vuL64 YryPeSguCFANzKZHRD9Z/KGi7Uw8I38HiD8NFUGQaJt4dEu21bOvpIAWRVQRFUopnJyccPr0Cafn T1k/OaY/WoEoFY91LE51w8WBvLf71NeaPw3hXbq/tX/6XQTBcXHcbepfyT50YHu/QaWgonh1PN6U u9t7/vDHP/DtN9/y7Ve/5fv/+or7m3vcoEgHLngFEUVEsOhBTJya/7V+xBQ1YfkSnp8eei/HD3+w 947v3B8iO9Ptwf1l+uDgNf5wSv7oh7vvzP2d99xfLgeOv2Z9vu0QDj8/5AiE0BdUhXEcwaHrO8Zx xN3puw5x6G76LheNYAadQBmNXjsw5c6VG3G8KE5PGYVRCi6VrQQY6bwwinLbOS4FNei0x2uHlA7z HrcRLYJXB+sQOmpRxk7YiGBmlLGjaI9RKZLKWYWxKFsEHTtEYBRnRcdqLAhGMad2PZtVx02BDih1 oGNAVagINhq4I9KxKc5FMcbe6YtxNAJVqF7oUFQMuo77csIoI27gKMqK3gulGu6VKgN3xbGjng09 F+tjRFac3FdWKoyyYpSKqOEYMgZA2KCMfeG2NzY+sulASsetQamOOqgr6oJScTcMGAtUAHdKV1j5 UawEX6EOo3bcd0rVjgIcjY5Uw90oFHovGMIGZVsKl0VQrRxbRycw+IhtB3TrjMMd9x5CXUaBWqil o3QdZTCkdAy9st2OHHvPWnsGu6L0QnXYAFLW+GrNVitqlW47cNIX7hW23QrWa+iUgYoNxlMTxDpq f8zdaoWrMxbnrhhWjeNRKVa4xxlKRzl+EoJfKoFbjlB1xrIOEOVrRi+MqngRVI1elFJ73AuDg2jH 5ugJygnjMKBFKNqxHTf0655KoVLwraFyxPHZU76/v+Iv9YbfjSOb0uHqnGxGrq3n5dkJW3FGN0ws VqkqmgrWmAV+W6wqghNrsLhTVFDAFFQAjdVeAMXpOkG8Yh4g/+h4zdHzJ/zkZ5/y+S++4IMPnvPk 9JT1ep3PrJTSxe+1Um2c5GopCijmhhnoW8Xaux0zsNiVVHE+gcUjz9JUeI6gLshoDJstt9e33F/f 8uov37G92+BDxcaKGGAO5rjndy3eJ1BHgCazQD0hxwO8uIR6DXynNODTBL9ke13auAmigrsw2ogm uKjbkXEzcH11zfHlJWcvzjg7P+fo9BjtCqbZLlEMm/q/9ZM42K5GfPQQkUk5NaU0K6p41/a5pGyt tSIilL4Dy3dRTYDpHJ+d8IuTX/CTT37Cz376M7758ht+/dU3fPfd9/hoKCWUtjUg46GEfAZsJgbe RlWyKU1j786D1l73uf/b34fe9VAfTF31oA/2jr2vtxb6Lrr4b+fY66u/x/FYXwq5QtyoNc9pzDGR MIBIIN+d1HtMCngwFeKO4sjoqBeKg4R5BW6o97EoXSleEa8UESwnm4mgQqBiMaiVtYdyDoELKCgj vduEZntXegcdDRPFcsIogtqIOhQviIXyF3FGie93taOriruydqVALDAxnIqIozJGx7ghDicAQ0FH oa8GrlQETxYHE7T2rFGgIgspWUP7s7KwlkZZ4dKH8nZn7OL5nTtrM6iWbQGpjrpnXzorFcQFqyFo qgqlalqPqWQEHGMUx0ToqiIGhsa4eVgJirIGqqX1aQWxWVCbO4ahGCLQN8E2OpvRuakVG43T7TYs z66kFRkA8sg6TAVTx8TwQVj5CrVCNYNO2TBQtYdeWJmDgfWgpqy1A6t0KDlZqCOYKkUFrEInbEoI gPXorKpSigCFdY3pve0d94pUyXExxA3xAgQwdgczo0NwMcRgXQudQQ8YxkYVN6NsBXXoPZSv9MKJ C+utIhRuvdKZYQVeD3ecdB2rs3NW4mxFOHbj6dNTbopzta78fjtw1UVfeeorkZzLBMBWnOIyWckg wf5gSFFco/3gMZ9xVKBTDWDrlfXRER9/8gGf/fJnfPTZx5y9OOf4SSgzUU8L2XP8m/bKZ7XZbJ5/ Syr1HyZlDwohnRVHO5bWoCQzEWtcKCIkSRDjIU6hYBU2mw3XV1dcvr7g+vKK4XZDHcYAFO5prNAk XgAMwnJ3a/ayxN8Olhb9LMEbi9B6QRbtnAFGU9SIIJqfJRuBxnnVAPPjcMH9zQ1Xry45Oz/n7MVz jp6eQGlCGFBi3SaDMSni7JNDSnWpkN/U/+7JiiHYhFgk+yF+rQFnocQacnG0L5yePeEX//3P+fAn H/D5Lz7j119+w29+/Vtef3eBO3Slgyq0biyiFBGqV6rJbPYu5sAEhNgnDeKMLdT8o9b8g3d893n6 qKI8AM4C7NBQ5Q9+1vseu238YWzNo6DqPZ//prtJskLL8RMmai9kXZ7tVlbDGp6QPJjGapVUgOoB OsJu1gm5iAeBp2kZdBYKXUSopDXpoBZCzgRqEJeIQ28GVEYlwUM8Ew8brSY0llTu0Y6g/C3pYDGh M0GsTd6gMcNtohMyFhx8xBgxFxSjqxYULgGWGnUZTZQAMxDK2EHdGTVYBFdFU7lXwm3SV6hSQdI2 ye9orsQqsZTUHQigpTjrGp8NaqHMSaC3GG4ntZRDMUE9FLSpgCtKPEuqsPIQMJ5y10qqDasUlE7A a8XHSh0r48bYmlMRtBrU2SoBw1MtdhVGYMi5FCBIksY1XJMm1WRfqk3MkgnTPMGNAog1UBeHi01j oOYUyzlhIFLoaoz7fR1ABCMYHhFDNOcvBUaLSW5jnLcAVJ0JXSX+FqH0ofVXXtKq9GBBqiHudBbU 31EClWsZ+NOwwV0CDB2fcFpWnFZDxy1/shsu7i+57o+pXYm1JOCuwTZ5kBB916Fogol5iRoxnlVB +o5OlY5wsY3jiNtIFadbd3z04Qd88fOf8dnPP+flBy84Oj1COkWaHbBgCKAprhlYzB80O7NxFq0l bz/eRai5+66obAJIks2BcA2YhwukFGwwtnf33N7ccnFxyfXlJXc3dwzbAcwCUDoBHhrAaFaKJ7Bw WwCY1o7F3zDP8ek1sm9k5m+kgT+VSbCG5R8uTm/+HwkZoiJ0LtT7gevhku3dlvu7O56+eM7ps6es To6oGNUNazdCpvEyYo5EH+0CjHdVJHFNWXxXOPS1ffYE4uHaF548f8IvT37B85fPefHBC776p6/5 8z//C/c39yBC13eIK+NYqWPI/L50wYAxg4mddi3fg8RR73C86Z1/bMV/sN2PAL2/5tlvHsdZfb/P 8WMBjqk1B+7VZEVb3TvgxGMxdpWOKh0NvVcxhuIUN1YJQ0ZiAjqG+Ii4YoT1KjhFUph7LPLww4Wi dTHGZjs5YbW6U5rSFZso4G1PCo5004gxpgIVgS7VbrxOmWaniYVQlRDMY/GwUFzovNC5pB+4glZM DJPKIM1aLLnCNH9WXGOSuUKhoub0VXA6SjI3o3aYlLQtBUk2BocqzqYLJbsahc4CQLmEf9wEBg3A 1JkkuApVPmiAjXUNS6YmXasuFJt9s6MGzaommIXvNvqvAQ+opVLLiDl0o0EVdAM6GNtxZBhHqhnm xpEEUxJDIAH+EnQNCl1ai6pJ4RNCtarhUgkVpagpijGUARdj04OYMmj03age4+6GUClAFaFKgLl4 Ltz1Gv1iSvGMs3Gn82CPtl1F3OlTgdWc3+pdWk86GbWGsJHCqNCXdEkoIMI2lb+j4EanQsW56QUt oDXm9KU6925cmbMypd5vKIzcmuA+ctsr97oC61gPgqsmwBBMgj4MUFTYSou5iJ9eNBiLThi1IgXG bcXu7ulr5WTV8fzFc85fnvP5v/uCFz95ycuPP+To5IgSoRsBWi2WcngEJn7iMbFx4HN781faN98g vIJxYbLol1eKNKbOU3ZEHI1TGTYDd5uB+4sb7q/vuL665ubmmu39JlyTHjC3gYZgpxJgmIWbyAGv k8XcQH4DFkZcs4Mp9ntE0gWQFwUolpm9mN4jPhMLF4OZUSSBkwKjczfcsN1uubu94+zmnNPzM46e nKKrgpYu5GiyLEjEmAQbd1i5vJNCSyMs7uKP3GkPVMxnkRLustKv+fDoA56cPeHTzz/lt9/8jt/8 +lv+8Ls/srnd0EkfbtJSqGMYJV3XYVjK3zbmTVS3EyxalXJrCYR33v5RiDLd+8Eb/AC9fBA4HHjs vitFmlGa82vn+e+o1w8xUQEMDlz7Du/0vnBify23/g8CSvbOPvzubIj6hNbVhW7UgtFNA2xJ35sY VQVzxdBQiGmjaloGDcJXQgF0C6sAMrALp+rcvmBDwjpqFlYSdGGxTe0PaxdvFml8btPgatDgYsEW EC6QUWEoIVhKFdxL3j8UWjTMMA3FLQhicS+xDhcY1dKKnbu1SOI0N1wKJgp0mASrIO4BQtLVg8BW CDgkYBJWjkkEUQUICkAWjExY4cE0EPSu2GxDegCUuC6NJW9MQLqkcCCsJxGICAuh+oBXow4jbBy/ c7RKBJel/7TD6fNhNadpMQiGRfFiwZbkuLb3grD2G+NjzZU2Aakcx+IMVRldqMUT7BlKjfs1lsh1 6sNRo980WakxUdXKC4M5Qye4OtQktD0oVi0le0NQd9YEu4IGUN0mk6RE/7k0d13J2AjFbWSjhqiy AlwKW12xVaea01WhmILDhTquhaEvWBFk1FAS2kFXoCtI0fi9FMpqla6P+EzbNX1BSuFoXZBaGV5f 4tc3vDw65meffMwXP/2Ulx8/5+mLM8pxBytNIW4B6xysGkhbcG8TN+8njt5mFU3+fnYtwUb7KyX4 T0vOy0Y2d3dcvL7k+uKSu8sbhs2WcQgWiRTkuGO1BlhIcFHdMUuWIkGNWJ2ARWMqDJ8Ax8MGzz0R 77aIuchzk0tk8U893JQigpihGoHTjtGpUjKwrd6PXI9X3N9v6L9/xfmHL3lyfsbR6QndOmTI6BEQ KdJYhodxFcu+feP40JiUXRS17wJYBt0uj1oriEc8SVFOn53y5NlTPvz4Qz774nO++fobfvvN7/jz P/8L29sNq7KiWxXqtlLHAUljMN4nRNTynaZ27n22fMf2+eNz9OH1/xrHfpzQBDB2zjGhnsfatmPp Pxjjw6DpbfdZPvtde+TdGI7DwOLRe+a/boCwiF1QHyjm9FJS+YXl2tUOMcPKTNsVoLfw5VfCxx3+ /fBtFoHOalpq4eLwZDpQD5dFowMlFOyqRqdsSzAozarva1DW2xIWWYqmWNho0rge8R8TClG6GlPB VKgiFO+CEYEcQQfTePf034/qjBqsSWfQubV4ulB2CKMGk7CqaUVL2sap+JubImIAQC0AVJWwgNMV HlhHAnyptRgLobOg5SVtblJxNGAoImFhDznkydqgmlZaDWBQnTIYbkRA69bwscLojDg1LcsuAzex ORq+LSNLNqvFfiS8AJoLP9we7lBVJzAgrnTWUcySgVK2JdqrHrSzWqHBACQAY0kwK8B6bD0fzxIk +smElQXTUyViTSDmMIAOW0SMWmRyb5hEPIt3SvV0T9RgKqpYsAsUqiqDAXRUa0DWMSmYdhRVNmNl VEGOeqwXxk6gi8A+LYW+O8JKQboO7TukW4KIHlmvkaJhuapEsHS2TSWCj7uxUvoVxx+85Iuf/IT/ 4Zc/46MXZ2gxnDHifjyihEQzgNE15sC0yg+7N5x2zeHP39cCakfXWD/fU2iZhaIaI2bmbO/uubm4 4vLigquLK+7v7vFqMDrmxLzMVqevc2YlkrGI4E2jpgA0s2SifHLLzEwGExjff+fJ1l+YiUswMYEK mWMiluc9wUanCjXjmzTZA4NxNDZ39wzbLbdX15y9OOfp+VNWx2s6DTaYygTa3/toAgaQRtOwtIhn w+ngWIuk91LC9ZPA7fjJEV/88qe8/PAFn3z2Cb/6xy/5zdffcv36KtbuqqCmzd5JlTSDoWkusFDK j7gcltfvf/ZDrn+X402xLO9yzze5SP5q98kjX92JF2EBaPYA5Hs/V2Shf3bPt8OTKZTU4Y1va/FN nSfl5+6oGJqm81acrVpGYhcKgkkKM9LyhIx1iNdziaC6UIShIF3TakTCN04oZEfYSgmLQpyVB4Pg 0hJNI/CxM2VdS7hrdIxgKnPENeMTwuqUFKSC0FVBTVCzZBaChalN6RsBSCRiP0KAVdzHVHQVxShO um8a+9AxSvABmt9vwAFacKEEWLIIBoQAFCaOabAWxRvdb7MgiAjK6FcLd0GbWJ4BkMXC2g9akQQg AT6ksSN4AMHRYFPxwdAhc/MsqP8qma4ms8WvdJGeRoxd69cdy0YCbEajBNearg1L5qQLHaAZNT4W ihf60YO5yJgSajBFpS0cjSC3bgwGx0qAtz79HENrb7IP6n1O+hoMmMX1ksxRl24PLUljNoqsg1pg 9AZIol+rWPRJKVTtQFZQlL4rlE7oxZFS8K6n6IojXaFFGNZQ10K/6iirFUdlRdd1VMBKgL0GHND4 W1QZLRePFlwC0I3SuBboVWAllL4HgfH5UzZPj7k/6TmSDFi1iMEQi/ViBgOGauGtimk5jj/yMcUl pIZxaQGbwQhhMNxtGO823Fxfc3NxyfXFNZu7O+owZqCg4hbz2y0U2wQYEmw0sFDdps8beLBk5OZU UyYw0twih8BFm0WWyncKtZVQshOIkDkLY+f8AmwYFcYasT2qcR0RP7S5umXYbNnc3XN3fcOT8zNO n57SH63RrjB6xKu1Rh7OCnmXY2YA9vXbY6wFQCdd9JWlW0kMKUTwpzin56f8/OhnnJ2f8emnH/Pt N7/lT7//E9eXN2l1RzzRI9j2b368mfF4NzZh/9xfG1/xt2ZXpnbCpOT/2nu99br5CwFiswEikUHU RbaBYlIZ1SkSQZImjnmNUL70c68Gn+IVTDIbBEHTTRtujWAhSLdJFWfUULK9OataWYXrHy/CkM8Z RYI6ThahiNN5RV2oaLAJOkJS/eodWNSbGMsWk7B3xYOuFhzXIRBVCrZRezpTjgZFvS6CVytVjbFE aOEq00EbvQ8lrb0VBaHPGhrogGvL/ujB+0jH0g1i0FVwEUYl/OhSI34gWYqimb6VynmaEGLpPgIX ZUhrVMQomVFQxRmKpM8+QWGteIXtaNhY8W1Fhko/OuIR02AqbHJWqCXvk+3BoSAUH0PZFUvoYhMQ MAGTkgG5hOVfRkAotopYkjIAgtgxXS30dUAZEbZRq8LWqCkmPSbCoCOI0WtN0JiuCifHsYFNwUS4 71ZUBJExarOknlRGROB+dJRg20YVNr1GFlMJn3bXr/Gup65WSA/aQd8VunIKZYWVDl8pq77Qh/cB KQXrjnBdU2wFnaDrinTBjEi64DaA9MF8eUbMt2j4FiboZNoWpWm+dLsFkDSErQr0sKlbvr58xc3v t3xxd85Pnz/jWV849ggcVJKK74QB2JjRCWl1vEHELOSH759+T8k0CaV8p4hFSCu1GuN2y7AduPz+ gpvLa24vr9nc3GFjnVyDWKZET0aTByi2dImk62MfXCxjLKot2QomJmIZ4Ll8T52s7PigGQPtnVQF t8z1EUnGNDIlAhgvwJOm1Z/nSrIAXfNWuaNFMR+5qVds7u64ubrmybMzzp4/4+TpKbLKIJrFIP0g S9onR/AOqmiJIy3+4bBVLBHQTEk2pVKrQYHSCa4SQeGrY9ZHKz788CVf/PynfPPVr/nyv/yaP/3x z4ybkMXaQE2yoUvXVZsnAukaP3zsv+9j77/DOiBvjFHw6X+PfH6gPW/r/10AcRikPKhL8sh93nUB TozFj3w85nrbDdrMX9QfXCOknNVGBopP1LNkuLK6TGmTEc0dBliVEPieCFGTa7Ckz4MZCFowhKUn arfIAshnFjci0DsshZoUmlhQ7SUbPKik62Hu0IiViLTIxqb4XndHkZcEI8j0USEDBFNJmmamg0a0 QalO50uzPTI4BKVz0Orh2vGwqIuH9RiRC9EaXchZk0hF1WQaNBd288wwsTWtp9prZAxGvpabxn0y mr1lVhQX6lhha9joDNs6BbkVjyBakSykJZrpkdmLGTdgXsFCWK+yLkMTRsuFOlOdEeiItBgNDWZF YOhawZyw8tQLakbJLxUxRAvVhaolKH4Jt4o4jKVlxyju4d7wZLKMwr06ozbjv1BE6aQgXbADqsd4 d0QpwSr4ukO6jlWnrLSjkw7pe8bjNayVUpxOCkWOEe2ofaGWTGGmglsWs1pT6Rmsw9TZlhGkclwj Hc/pEnyNyabt+j6bL1XzPZuboM3cMgV8SiS8SIeu4MIHbr9/xdX9Hde3d/zy5Us+ODnhaX9ErQNe A9xFZGdQktLm2AHp0+IF20eyaGB8b86WsAPfb/eYEiV8FjwtqFhc04Lp8Grc3t1z9eqSq4srLl9d MG62+OhQLV2CRFxQdVzBzam1TtQrHvFOuGUwp0/KyjKYs7lCqmeGbXONTK/nOz/bBxlLOQF8l2wD aQXnGpXMhGt95uoUk4nZaJkj1QHN4l+iVIvA9qIx9uNYg70Exrrl8n7D3fUNm5s7nr14ztmHzyhH fTy4pXRKa08oqdbvj43NTrxF+8msvN6EUbqux6sz1hHtlK7rGG1gGAbQiJYTB4qwPlnxk88/4enZ U85fPOfrL3/Nt1//nqtX14s5FY2dgyCj811kSkcWWQC89r09hf4uwGI5ro8ef4VZ/64ukL+OrXh/ 7uHHYkkmHXto/T+4/WKCOaFHqMj/9g//4K0+gEgFHLeIcTBNlVo71ByPvBEg61poQIvI44DIERGK SSrLmsoxfezudGahDIUsxiVpvUPmH9BSXIP9UJwypQk2xK1ewLs8P0wsiiyrC0qd+iEi9rPC5ZgJ tdKCSm1OG4QsYgXQhH+0ziV9xu4gFZMx7551QiQoac/zJQNEq4TtqlPbM421mWqNH3DNGIcQZJpu gZn5ybblojSDWkdsqNhQqeOIjAEmIjYkhnxVSTbJGUWieJlZVHsURapN9K/ZGItajTGzKdR98n2L JwuQjJVKpZVkKh5R9q3+iNZuisMxccYyBtiQrAnrGrE9OuJudBYNHlOodmWFFqBLWrkUKB0cHUdR rC5S4rpuRd91EQhUCrI+gdKhCtIXrPRIUYoS8TVa8IzDidgFy/S/yJqKaprNwqqTEjU6WmZQgMaa 7x3/IshVch3kWjugBGKOz0rdd65V8BL316yPIiNKZYVzIsLHJyd89vwZX3zwkpdPjjgWQ30ERtxr pkgL4pnaS7oIJLIaqgOisdqSIYuA5AwgNA/3FTKB/ql9yJT14skYiESFSneHsbKSgrpSh5G76zsu X1/w+vVrbq8iUDPKxnrEF5hnJgh77o2MpcgPLdkLT9BgC2DhbtQE02YtfoYEF0xsRft7Ft0LF0lz 58DkMZrAl7Q4i9kF0taL5FoQjQyRIDA0+jfdISW/P7lHhIjPyQJcItGnpesoq46TZ6c8+/A5L168 YHUcqaujW7RLMxMs+0bwNDZiLM2SIhGZ37O9vuy+++EjM+gsY6G0+dLTgJKWPRPiWUQoFNzg/v6e u5s7/vm3f+LLf/yK3/z6N1xf3mTbwEfSrNRgotL8bEbSMgB3MognYDEboW/NgvJlquv8njvgJU88 nkvz/oe3Bj/y99uOtwbsvhOL9RCg7PTa5CuLBbDMhhHmtdAsokMtEjzdIfGsdlX835D/9A//u+80 JBfuMuVqepE2WL77kvtoaf77IUKWvEXOt93P9jr17QhsXzy/+Vjgq7kN+7SbMKHotigk29KUxBwO N6GYHQvxMV+mtMozec99CkycqRaIQfiqEUQKWBTowR1GQpBmBohNplrEZYTLoWVbEGyLkQARahFG DyusuERhLxXoYLRwTex0b+uvnJ4RnyOzFTT1QjAVNX3fMvVLxB0EQxEgzUUpXUG6HukKpY9gSSmF 1ckRUgpFO7TT3YDIUtBVHy6HUqAkK1MkhVRk8lgzRSHNz7mvVef6Em2ewrz+l/MffPHNNy/6H+No xdMgXIemC4oeozPjCHhahA9Pj/n8xRmfv3zKy9MjVjKCDYiVNLZtLvhEs2gzRqrVcrDI/lK3GVwk kG7zOpxScxDg1H+p4IP+zyTx0fD7kfurWy5fX3Dx6oK7mzvqMIAlyK08Kqj+AAAgAElEQVRMYMEs AjdnF0elVWiaalVM7ERM85Z2Ov30OWOkmiXAYwITO/EXvlfZsnX8UoDm3GkKbg7aZGIplueVBhSY rmnXzeAianjM2SjMn2vLLtIA072yPj3i7PwZp2dPOX56yvr0COkLo8d8mLNKfIpHEVoA5m4Y3qF4 jTfJ1n2l/Ph183oSCXeKVaduKlevrvj9t7/nq3/6mt99+zvuru8oErUxas1qqQRbbg08SWR6yVRt 1Xee5bQ5zONClgDKh97zAbh4Sz/8Nce7unMOHcsYnsc+f/t9H+rGJbiYGawFG5fPbKzFW9sJ6J7l 1AJBJdTJbgOWi+xBugxpwuE/4AX3Ormd/lGOHzYx5MAfB6Nfm/AgVae3TotjsoQa2tsDFo/7+5rS nt0Gy1Y14JWhhrSaiZr+Zh89FvBQoVHI1SbrS7UL1wOLc55Wss/PiGvTtWKNtVBqjnFHC8qNeg8N vbpIVI50T+U9K2kggneL4EcrrBO6rqP0HV13RLfq0b6AKv1qDUXRUtAluEiwMIpjKqiUSDWl4Blf 4qoMWem1pcM6yTq5Iyj9ogZKG9cJHogwLvy+Iosx2P0xj0qzAv9Ggmj3aPlYLcBwIRI92LehGq82 EQx4fX3N1fVTPn35jA/On/Ls+ARVQbyC1Sjihmc8Usue0MlaiTiEHNsULObNTddcpgm2s9vMmtNK 6DQDgatxd3vH7c0Nl99dcHd5ze3NHdvNJgAMElT7OKc0V5sBinuUTHd3sFbyvgELAogs3CFTtsj+ PwiAsQATb6PVl8dSHkRQ5SKoMxX60ifdYjDUbXIFNZ+zaUU1mBRRjcitxnSI4It6HKrBHDmC2xi1 Ma5vOXpyydmL55x/cM7J01O61SoDPueU92xgkEFZ5rv5vt0XisKZLdY3dsXb7flD8QOiEWPSdx1H R2uevzjnpz/7Kd989Q3/9I+/4s9//DP3txtQohx9CBNwWHWxDUXNsYc5ZsMn63q5Gt4Ajg6M56w0 D4OMQ9f/mMff6r6PH28HJu9y/uC1TXPJw+/NrN4ELhZNOoByl19stMn/H48dMPWO1zWqcef3t0wg meoQ5Pea8Etg0kmmlNZAlrVG9ocNNQTxVIFSJgqrWaERDe8cdpbNzyulQLUoviWZDWNR0+KuZo67 lMx0SJOsCHRKd7QOn3LXR6nprqPrIuXSi+InfZT0LgXtC123CoukBA1M6WawksxGlWBsvETQmKsk e+6z5ZwDE/U9Pd1QLFjSCOSqoy1SGOcxO2QRPBa8tPze3+dI+OkzU9YO7XtUhFpHXt+PbP75e767 vObTjz/gs49e8uL0hCPtKFIyJiPqwHQJpGarvrFIFRelVW/WUiZgPcHonFINqGoWaaIaw7Dl9vqa 19+/5vLikvubyPyoYw2LOuiDaUxa0agGLhpr0UBBxJAkk+EepfF9DgZcBm+2oM2d87UeZCyWPw8d BwXl0qrL/tkHF1OQ52J/BcNiLyVv4KOBiHBFFtWMTZ2DUd0jCLZ0irpTtyN3l9cMm4GbmxvOXzzn 7MUz1idHdI2pixkxZdnN49XGbjcOoCnXv1bZ7Rtly0yZatH/ZV14+ZMXHJ8e8/Kjl3zz9W/45qtv +K9//hfG7RaVPia3gVdLOBtGVWOVJ3I3gUa2epJv79r+x1jxtynbvxXI+Nc+lsbx/vkHf/tD2bnz NwkwFuvjwbOW4GLfrfFoIw+wEf9/O6aJN03mmZaDw/2zz9IdYi/aNVOxnwPsj3jEBfjoUVq31lh4 1aNkdwqgCFKdfatBMEgI8Eeok3nBRKCnOxnVLdTMrBjXHU8//wTvO0ofbIOWAA3SpRui7yLQrOuR TtGui7oNXQk2I4WCSLh32k9vEqLoRM1PwWdzI4O9sabQdJrInu/ZhPJScC47XVrkMQ+FyNvAxJuO HytY6u3HDqnd4AANZoxG1BWQFd51jHXL7c3A5Z++51+u7/ni5TM+fHLM+ekJR6WjEGnZUyBxBlpP EUmqmf2VfR2USViNafG1TKyoiaNYrWzvNtxeXXN1ccnV5SX3t3eMw5ggIWM3LAMxc78bHGwMkBxu EE93h09xBJ6UeQCKRZEsT8//lB2yCxpmoPLDQMXU097mWVuLu4B0H2gFA7ELXEWCXQs3QZTpVwnm xUsyFWJU02SY0m1ixN4koli1qJBZClJhc3vP3d0999d33Fxecf7yBcdnJ6yPj2I3So8y/giUrkxA ThIezm4CmYIqZ8m2B7YlZlxbIm8DY4cYDAQ0tu1lGAa6k8IX/+4LXn70kp98+jG//urX/OF3/8yr 7y4YN2PE61gwW1qi/WYRv9eiZKyB4YVx235vRsdjgOHQO7zrWn7fNf+vz1Q83o7laM/Hgs1lVhn7 rd03uBNSH7Rd4/McHbMAFz+oAxZ06r/V422odXq3R17xUJzGjmcJ3uIeybREX0xCEhy4M27GcHuM I3WM3P+YAElHNZTigMyWmxJ+X2eR6bGgqZrV9aAtIuFqUEVOTzj75U8Z1z1l1c9VB1VAC1Y8drrN d6wCQwreqP/hlFaKPCTvIpBs9tWH9aQ7brIotS74mHtDZEAt6f9vQrIXRdveCcshzE6vPhdUOoTM H5u7++cfZe/+hnN/LlQGrQbKftaHpaI1F0xjnLZ15P5+5Or+NZevvuMXH77gF59/Qv/kCSst0XfN wgTILKq2j02MU2Q/1Rbgm+9bkHm+mnN/e8vN5RVXry64urhkc3dP3Q5gs/vIfd6VtMmLcZzZDPdg MKLCZnunxlwEuFpW3txlKmbGYt8tMrlKsq9+qEukzalQaT4BjOU10hgUmfvoUL2LVoDKW982N5QI UIPZc4/5rRD1ahI0m2GVYDE0HrS9uePVduD2+paTZ08is+T8jP54hRaN8HmjoUImF0g72rm39QOz ODs03w+tkx2QgVN9pKjSH3VTEb+TZyf8+//x3/PxJx/z+9/+ga9+9TW/++b33FzeAEopOs2dqc89 7helEpPCmCw+ZtnBw+ySZfsOtf1fw1j4e4GMtxlQBwiKOE+el+W1h/vpbc944BZZHm8apOUD/xpr 8O95PMbWvAkU7Hz/kVeeKiA+wmboEng0atgcrylI74fY9dQiPbRlwLT4gIZFvUlBJoMfJOjW0Ou+ 89xo8/wdLCK9JVM+vRTWT05YvzhDVqssox1+3Ii9UCgeljOSW5hk/LdAIcqxa+vbhRCIR7ZAuMW7 uEcfEAWGTGI7efcITKPNLSHdRM2Snsdv2nIrKZzRbHr2m6yY/1YBsk1ccBvcZC8cWuEvLVF7ZZSI i3HN/XHceX19xc2TE8Zao+5DZsMokcopRZnoCSLFvMXQiEdhqOJkFoKCGXU7sLnbMm62vP7uFdev L7m9uGLcjtP+IBiMtaJdoVpmMNk4gaXW38tMj0rM/akYlrfskQVgsKXroLXap31FmD6TxecLYMHc lW87JuFKm0J5t6R6J8DRbpnn1ed6FyqzfJxiMtRRcrfjVnuHZDK0cUmx1wotRd6C4em6CGzGwIaR m8srNvd3jPcb6maTu+GeoCV3J2ae90vW5cFakPkdWACynHK7x47xJA/PpVxpRoOYMYxRdwYJV0/p FOucZx8+4+TpCc8/eMHLDz7gm6++4ft/+Z7t/Rgu3+UzFkerR9LafmjMphFzPyifG+2/t8J2Pn8b eHqf418TZDT5eLgde++2BySA3Kl5vlcYI60PbOcLO7dLCiTku9K9qxXXzj0mrP+tAox2/C0GfUL/ i3NTGW/CCjTLCOuxMo4jOkZqYLNAGpyY1rNH0GIIsoexGzO1Gy2QSSjOQH+iFRtElainQV/oTk4Y S2FMiyloZk2wIHSiU4SwIIg1FiZFvvic/pjzcEpRSuEbQWdzDzU1h88Bo3V6lxbcmrEYHpM/cQd4 paK5aFJw71mby775tzBPd+O1bWKs4i9L8Bhpv5b+ixDiipvy9MkZZ0+ecHR0FNZgrRh1Si33rBsS m2VFwaQsbQMebrmS889sy/3dPVcXV1y8es3txTXDZosP4eooGqBiGMbYnVSE8W6IfWsAt9iO2xcA YXaJ2DTO07kEm23x7AKLABd15+9Z/jSD3RdrZerTt1jfsKuQYRm4zbyOJOb7pKw94igmkZtKTVMY N0Zj+dzJV+1zlc92XVVFqBQtdFFilmGoaK1oUTTXZb0feT18z831Da9fXfDsxTOePjvj+PQE7TJz R5jWrSzew30Zr5HtaoD2kBjcO3dIUUpzWXj7SyhS0BJtqXXExCIzZqVot+aT9Sd88OFLfvnvfs5X v/qar7/8Na/+8opxY5MRIRMCiIFtpQH2mYp9ACD5Pm+T6svvvS326sfQEX9P2fPoow8AjMe+7xNS f8PNMjW6awu7lDLReHGjNz/t0HU/lILcv19La/tbH+/6bvvHtGna3uLShfKMdMhc3D6nr4Z7QlJ4 Qx1Hhu0QNLF5pumlO8Gaum24cQYRzRKYWri0OJbWehsfa8xAXi7gGtHsKLSdWeg6BoTj8zO8L7EP RwKFkFMhPFraIGTNjbhremji5yjgGu8SAabJWHhsOoWyEERzoSYl9jgxmQuoxbNTsDRh4c26yTxr 93iLFEQP4jimvjsc1Lkcy+X47p9ffr4/1/ctxTcdb7r/vtBvcT/RV5Yl2pt4jcj6TgoiHv5tMV6e PeXD5895so4dU6UJBIWRGtS7tHTVGDd1oQO8htuJ0bi7veX16wtef/89N1c3UfjKBRsr1IXVktmj LaOjmjHFRixqUTSmIXbNDOYhgEIN90hzi2Tt62W8xQQkPAJBmXqBBXORZ323b3/IeACTa2X5WSg7 mee0SNbKSYYxr51TQSNDZGI0zNESG+GVSRZoKF9r6c9EAcEsVGUJ9JR535wAM5LEk1DvtlxuX3F/ fcPtsxvOX57z5HnsV9KszYRCtADdVvxwliWJypoR474zCR+Tl8u+mftYwLL6bJsbWfgvagnFRnUC aK8cd0d8cfpTPvj4JZ9/8Slff/kN33z1La+/ex0756boi3nAtPtsAIrUVzan4YowzcGJX1o0bUfD NtC+kKcNNB5iLx5b238rNuJNeupNenr/zNKQm875wmiVXUZjiudr+o7ZuPGWwh6NyO+2byWszDnX LVHzfsMffel3gTlvOQ5ZkEsq8V/7+EE+WebJ1liIib6FOeqepEnTsvdqePWoTTFWbIjy1RF5LzNK l4mMak9+vN2PfaCyX5mVVnQ02ji/h0Gkeq46ylEIpeY9CUIgFaen9eYOuU359LjFs7zV82j9dKD9 +7SrpvLcv9f+m9q0UGw+Kzb9zLsdusGife8mDP4ubhOfVivzPi4wB3kaIlGWOVIeoUjFa2WN8ez4 hI+fP+fs6AipFccgd611IVkLcqsTwSsRbGkWaZLVub264ubqmtevXnN9dc2w3WLV8MFix8wWmJnK 3o0JONTcwyZYj7pTGCtYsMgKaWmm7lH+P4BrujZsBhMPmYvDDMWugts9f7CbD4DJ3fgnfyCPpt+T kWjAewLbbZMvZwIKrQx4sInSHIPTVtbmnoGcWT9DBbPILqnqdGT1V1PCjRLbNcT+SjJVNr0fopje 9n7Dzd0tT8+fcvr0Kf2qJwr0JSDXYD4sx8JT3uwYTP5OhuxO/039OYHhkGFTAGYGCztz7RUBrEbR vqOnR3z+i884PTvlw48+5NdffsNvv/0tt7e3IRsTULkZXR81nochsooirV5mt5osxnQ2SXbAxT7b kUP56Pu98/u/4/H3ZFAfgI83XLncM+TBtTJ/3thxyR4XyZiLyS/3Lh2UNNuPcSw7+O8BLH7ohJjQ XkifWCBLyJsWYuwKGZH14ebONNJxDIAxVKLuedTgL5RU2ikQJ+t1z+1xoGt2rNyFcIy/dbJ44/u5 tbiHcFHmfq8IenREd3yEuOV260yxHiKxwONu+V7OLLRyWljTjT4DCFmAjXbsq//lq01ZNQ9f90Cx sv2rdC6u+W/yeNwSmYpYiWTlSqFX6NyxceCo6/n47JQPnp3yZN3TUykSVcExJmZApIRVaFCkoxPF tgM3l5dcX15xfRmlqG9ubxm2UW1W2uDWWfmbzcreGmhIcCEP4ihser4twEXbTCyUcgTztSGdwccu UJgBxuJvllg3JqDnQtpf5VFGfOdMCMrJlZB9vZRPpAXXnmmG7bjfhLYXSuBfD4VIZNnEzrXh1pt2 +C2ZuupZQIsEDKl8tW2fILlpgCvFHM0JrlUpPVPp8c3tlmEYuL675fLqimfn55ydPeX0yVP6dR/r vEZquwuRGj6JsyXY4gFz8a6ysgELWSxUQaZYL0FjqwEl3JySAZzqHJ2t+eT0Y87Pz/nkk0/4+e9/ ztdffc0f//hHbq5vcGv7stTp9ybj6hi+tFJKMCQHwGMDTixeb5eZWqy+fTcLu9/bP3zBFvsbrnuX Y9+0fPCsds0b/Rxx1fKSpvx3Wrn3MJkmhE0AsDEcSyC2z3Qsf7b11y2Bxbsp9rdf8zY656A1wN4k +BseP8b9G0WbzFBYIll8K+oLhJVnQ1iVNkZKKdWmgLC2xXqLKQBo2OVdjzf6CdOyaTEYIhKLOn29 RcpkUZnC6vSE7vgIJDbFakCnpLBtE1pSmFnufBj2iE+UpCKYZwp7/qwJZpofem7jD+n1+TgMLObP fuh931WAHmLb/lZHzJEDgCqMWERivxWpRm/GkyKcH604KcJaha40hdVYhqZsnU46Oi2M28rV5RVX r15z9f1rbq+v2W4G8GQ4iNTI0XJzMZGou9JAgkV9ArO5TsUUS7SX7eFtXxBmlqMuAUcDEAniJ/bC ZwW47+qYgUUIU0+AP/XXgTE9OMzNaPKFWyQVRgPXunffib0kn6k6AQwaUGjA3MN6d5nl3cRsuu/U wWh1atw1QExLFXbHJXZURkCsYnR0HZlR4vgI29t7tsOWu+s7rp9c8fLlBzx7cU6/jqySiNwBN2Ha 3vqxObhn/L31uibJluCEZDJS/6rLlCEkIkiRZLoiEPv07ISTkxNefvSST376E/6f//v/5Vf/5Z+4 eH1JHWpUO+27qOtTK+mVCrDBodiLh/PgUBzM/nUHXQ6P9IPEhwef9djxvsb08lnLdj58boMhD7/v +csO+Ghgoq2h6aL588f6R5DFHIj36pY1Fw6BjIO/7y3wH9JBh679e1JEh463LiRfLKKmdBub4VA3 2wlctJgK8axPITrFFiCNCdHpRpKUf+MIcqim5+4ec0pcHHqw7XuGSAiuPFOJnTC8FFZPjpGum7Za 97y3pQDX/J4nQPEI2piUheXeD8Vya3ufM2OakhM8Y0oAsQcukGBW5gqVO+enPniz++N9AcuhWIr9 zw9de+jz9z0idmQBvqbfdHYGeQA8IXa/7Q2OVTkRpc8y3qV4fj7GDsOqdKVQq0MVxqFyc33D1esL rr57zd3VNeN2iLiZ6hO4EKIkuVtkDVm1SZnXBQtRE1zgHu4AbzEVNjMde+DCpvLfizoW3t5/4QZh BhbT+GQ/LBmLfar7TWP5pqO1cX8063I8EhTIQnZZc4O0z5kxiwiglnIjowFqq4ER+4Z4md8hXBcW VWk969lowVXpWjyDR6BkdYsN/DRkiyIwCOO45eL+NdvbLTfX1zx7fs7Z+TPWx0cYxrYOsXWAgpR0 hWYU9mOK9W39GTvQ2lxnhhaPBc3lp8nSmBmoT4xo2722Uil94ahb8XH3Iccn/yuffvYxX335Fb// 9g9cXlwxjlvEYp+giFvxzLar4Yh5IzCYLfoH7pFH9NMP6Ye3yYH30ZvvcsztAtjT4yyZBQeZsz7a Z3N8ILsWTbv3NNcfEgJLliP0oQe4WAZztp0I3/gSb/jsfVmB/xYAxiEg9aYJVGZOEa81LLexYpsM PrIo6Tv5q7wtMmgwPqx+FlU1Dy2MN0/4Fhchvgj05OGABw2Zi8zaHhEBarTrWZ8+wQqxc2q+2rRN 88Ic8QaERJBWLdNzB9osiqSeAbD5+iINPLWAxHj0/q6bnor1sd0449CDrpYHIOo9jh9EAf+N5muG 2O6mEEOWPA+XlmjsNouF6+Oo61iXgowVFZ9YDSQYjCIFHyrD3Ya72w1Xr694/f0F99e32DDG5nUW 5eURjQymVkbbE0xkUTdYgAvPOIoFQyFZNGt5rl2/ZDKmz5gBqvtcZG7XDbIHLKbzD9foIYbj0PEo ODygeKbf5y+TK2FiJ3bYjOz7+LXdJxiGiQnxLA0+PTdcOSog6V+sJWpfuIaTpKBR/ZMEmMn4qHtU xfVCqULXCVJiHO+Gmyx4dsvN1TXnL85Znx7TrTr6rsPEqV5zI7S//nBpqeDNKJL5d5gGMDbYg7aP SGNFhag+KyL0Jz0v1i949uIZn372Kd98/Ru+/tXX/Omf/8z1xRVWK30psS4sq6Imc7oc4wdud9kd 3/dhzf8apn1fOe8c79GG3XPwQGccIDF2WIv9dh1+2OK6vetF5uDzvK7r+x6Ya9KXUqbFvX9MnXjg 3X+MQfl7AIz3fd4kHM0DUKTgleroqFPmR+CICH5sgnQSUYu9OSa1vRjAqY0t0W3fj7nsPycVbpbq 3q/s1S5LUGAa17gBJUp6r09PMJXcNpxpBmWcPO5t4mVgJ/PfiOMauyPspAomuhBZABtvu4YeYB4E xPVBy+c3n1mLfdajuRDeDEz++uNN8/THcum1TJg59S7GwUWpBLXsJHjQQt919FL2yp5n6um2cnt7 w+3VHdeXN7x69Zrt/YANEQvh1aO+gNV0aQVtXtueNjazDxCpxC11tAVnLsEDblPq6bzh2DwnqtUd F0irOtC6roGO+S32AcZsobn7HNorksHGTOvtseOwu2Rely4ttdIel1fhv5m4iLZ/SwMPk9EAYbFP gjhXeyrSCEb0ye3UpS5WVfDItKgadWjMjbFK1iLpJldAbEqYfY/AOEZpfpUosLXZcrHdcn11yeXF BWfPn/Hs5TlPzp6inU4GAW3H28Wr/hDADT6xFy4Zb5IgYnrXmmPYZF0b2zB5adtSm1kEHncRW3H+ 4hn/y9n/zH/3y1/y7Te/41f/+Ct+/7s/cH+3BcItEkljD2Xocs3KNHay8/n+u+67Th47fijIeAB0 Hl7wTgDjXXVXU/iPf/4GoPPgPgujNq9vdTFmoJEFnAFubm64vb2l73tOT0/puqittbPAf+BCfWMj 9ybr+yLHR++/1LkH+usHu3EWAjvmfwS11bHOmR8W9QjUgkaWFDQuMIePyyQMmyXf7troyKhOd6gl B6Dnso0TGZCMyIRslupYklAV8Ah8qgrWdch6RTlaZbBmxE9Eyqpkam3ef+qKWZB6Mi5ICEw0LdTq mI9YTS+vz+FEbzpaqlx7gfYqbxu1x4qa/RjHvybojTgZZ9+9I0ikOPoMR4sofSn0ovQirFDWFEps J8K43XJ7ec2r//odV6+v2NxtYyOw6pEpkvPTLOa1ERu7jZYukGk9BiioS9Bge26OPXAxuUV8Fxw0 f3vceo+V2GMp2rklOIkUxKWx4ztxNg3Qtu9O/bfHTB4EGAmCp7/z/oeOfddJqvVM4zuk0JiqlqrO NS58sY5i6gtdJmNJ1qSY3FTpEgtxYXjG3rhqujGdnhIpn8OYACVdmeLYAFevLri/veX25oZnL1/w 7PkZxyfHiGqMO8GM+bLPkg5/N2rQUyYse1GmX1Waiy/AbfRHyYymVp2XKPZGbESnKqyOenyEF/2K p0+f8tHHH/LlP33Fl7/6mu/+8h3jMMJE7+8CJGW3Pe5zam5cvff3e6z3CTQs3vpN1z76nB/JQHnn Y88ObUBh13idwdhDd0tz98U4a/7rAC4vL/m//vN/pl+tePnyJR+8eMmLly85Wq9xoiZDq/muuqC1 Fw9qAif8hfpAOCyPfctkv4MfUxKzsH081bAB34agJ3XWFD2zEFji20lBS6NlIz5CkoEIZsDBoA6R 9VHHMWhknC4j7XSa2E7jHELv+vRu0mxRn6fh7OG2mSGYlGxb4LbLIEz9tuujn9SBEBRl9ouY0zkU FbbV8F4ZcO46eHr+FEqJwjcJnozMS49KWdNRtJBRglhbxNqEstJJZMpUHDNl64ZbhazHYAleJqiT VldUi5OM+VjUCFmmqMrMqk0MxcQA7WbHxLA/tELedhyyWN4X9L6r9bM8PNmb6fWI/qoOhUgZxR0Z K2sVTvuelTtrUU67FUdeGC7uub644Prqituraza394zbLTamYM0qmJPV2P6ZMZpRHfKi3DzMYkOq HCvPWIm6kw2yex8znzaf2wENU6yEPNjZNMmAWBEtG2Vhi8qUSuVTdtWyn0KBW1rBcSdr9836LgE+ DoyFNAZln6HYG89J0M5PzlGKtueusS3Kvl1m6pPlZ5bFtyaZSTIe8T7VhDnNb+J2omhdWvbuUR1V RWINtD111KgSbltFqVVRLxE4mazBcL/l8i+v2N5suL+45uz5OU/Poj5GLVDFEjRauoPSrSOZ+ZJz p42X7LBPS/ksi/7Jdc3M+rQjI3liq4N0J81YJmRLJeSQitJpz0effciT8yd89PlHfPXll/zu2295 /eoK24JYT8nif1Yr7gHEZ/aYSQ4h4Fl7pTV5YjZ2BOoMGJerk8XcnUsCPozZeTjXmFi5nY/aPHvw lSVs8am/Hd8hqlvvT/py71Yzz7YYuz1ALTAxba25rbKsLPSVCFNdi8Zox/QUyr//D//h/7i9veUv 333H7c0NFxcXfPfdd3z33XfUWnl2dhYUnOrkOmkdXEp5YGU81inLf8vrDiG4plNbaVLx/f55fNjm tRyLc6lop2uSjpw6c1ooMWDtU81sDqmxE2ndVmybJWrHSCcNvRs7HapHONWkkN5Zn/neT/b6ZNFH cujGe30oGWUhHnuCSIt2h+KFTkpYrl3HRhw7Oebsk49YP3uWVTDb5A32IlLqFmKiLYypebHVOhqT q+CUBKKNlg0BZXNFwljBqMd+IsIcw+KyP9qtP20WDMveWy46OSy5aqsAACAASURBVLyo35d5eOx7 73K/x4DF278bbFEM97wAWnqfu9MJ9A4nXceTvnAMrB2ORfCbG66//56L715z/fqKzXVsKOYZcNvK qE/sw+Lf6BbukAy+rD4HazYg0bI8pnMtpXS6T9aF8zn9Nf7JgsWI5bYEF3Nfxf/23bOHDJb5J9OG Zy0AuX2+jJmAQ1U/38ycPHjeA5k1v5dAm4aLL0+X7Y7ywXkwj7FP99398tS+7ORlXZCpnfmVVEGL r0vreKw643bg/vae+9sN4zYy2kpX6PqOoqlCJ0WY8/Cx+esNZCi4BkBexFssL/QWgNYU+U6f7P/b 655Gxxfh+GTN8w+e89EnH3H+4pzVqud+s2Vzt8UTPGgavKVoFA9MsCzCVJm2AdhSCrH+5nFuTUhY PWGONu6Trm8dL0wZcW9a68uvPXjFR781N2v6XebxnnrNl9c81J6axbAe07+614IJWIg8GJ2WctD6 UwmZ3gE8f/6c//gf/yNXV1d8/933vH71ilevXvH06dMdFqJN3lLKFPjZAryWsRpv9d0sAMry+h1/ 12KBvj1LYj6ytMTO3/vXBshbWDwN8WVxoIaasRq7kQ5jpDxlyeMQIGGBNcUY7oi2uc7hd57a9IOt 4BbZ+24KUhPp0yyA9u1kJXAyxz3S0lb9iifHJxz3K268MnZhBU1psgviIsatBRfmmMlc+Epzhldt VQs1qkRWwUfHhyiiE5R7dqNm1LyHBaO+GzexzIaBpZeq7WOyGHHXv6l75F2ONwqUxbx/wx3m6+Xh qDfQ3ZVUAAnQMOfm5obt3TUy3DMOw7SrblN47kwBdEsla+ZT7YnR6qQsl8GXu9c//HtSbum22AUW PoOb9jcpsPeVeQMh7LlPm5UYf2R/ts+y33yRdjtJ3nDJZaZj3md/zKAxgs1q3V2mkoTF/jqcrxPJ FGz3qZBeC9QO1rMZV+l2tHk/kvZ9kbY3SWZYiUylxB3ZSW/1TAnXaRuAzCzJEvGmURND7f9j7s26 JLmNNNHPAHhEZmRG7rWRRYnqlihKPXPvw32/58w/05+cudNaWy21NrKW3GJxd8Dug5kBcA+PyCyS 3We8TlRk+ALHavaZwZZxmHHAOQ8fHCJHdDGi7Tq02y0eHu5xvrnA2fU5FqcLzEJAYs3XI0ObGbwI DNV40pCmEo86Wccpz2kTHmvaiBEQ3ykBSCmC4OCDV80PcHl5gdPTE7x6/Ro317/Hb/7X7/D3v/4d 200L7wOQgC5KakVyhOBs61/ph7neR+373FCrF5dvRsUkym1FoTXtrXKI7u9uNex/psK3er+6Se/M 6V1QQTvXJ/jwxNo4dBRthvWdBvf75b/8y6+89zg9PcXl5SUuLy9xdXmJ5XKJ6+trnJ2doeu6wSJY r9fo+x7Hx8cZoZl6L4QwaRB60KBk0LE00FQcbNeei7ZfCNJ9veodRKT2EcjqrQoeSMf24uIXtx1S 1+VtEGOylk/DlEvmdpkJHoZhzL/r/t3ub8PLblT++HembKpNEGplBmOCBggIHpEIs5MTnN9cYbZY IAWHPjj0vgATH1ndSbWX8hYICgghgtc+JcfqbSID6T2Bgocjp372UK4wAmNUmYjycDHUSLl6oExq +9B4J3y6Pz/lOKyifN79n1YXW+RFHBKiXOawJ8LcecyJMGPGDIBnRlqvkTYrpK5VdbAgCmZG6lMW Ciwstxhnsuy1p4Q+RoljUWkzYozZiLP+TIEMZtbUIFPRNVEZhuo9Iw2E/F1J5/ncWFPBhbGNei/t 0ONPG3srcZ9dxqFys6YABmTK+eFd+teEliSXUiGnWpu4W9IuY7H7DXhPaWlMwrYIzSlGtNst1usV urYF9+Jq68lpzJTiOSP0tQbBheZk49apdTPqNhp97NohUcrRUPMMFi1p0zQ4PjrG9fU1Xry8wdFi jsg9Nps1YuzEXVeTMcrcKUIOkQM5dfEl3RKv/jnVBu/TwhdgqMiwAk3j+wb9MXWuavt+zak1fXdO 0J7nqLp2iD/tvr8SUivNhRtpMgRUiNDqiOD/5b/9y69q99P5fI7TkxMsl0ucnJwMOsZ7j7Zt8cc/ /hF//OMfcXt7i5QSjo+PsxGobZ0cOqYZZ9UyViY+auhuQULGhh+orQEUXOSX5HIclNGZJJAAjgBH ARXctuC2B3dR8yhIQBwPp88VYCJqNSNG0xWeGsjxJJ2atLtl2AizLmIDFdL2PK8rWw52w/fJ314y moYGTAQ3m2O2OILzHhQ8+iB2D2KcWoaFnAOcQ3IwiFq2kEDwSUP8OkYklhwK2duV4B2hCR6O1Cff qUUMV0ScJH6Ah/moAFD1av3PfOJzOmuCbJvQ9Iz5PuBi3H9Plfd9wQVX4ywCOA/OOxCCI8xdwAyE hhNCSuC2Q79+BG834hodNZ9HYsSoH2X49RZH4gpEmK1EzhNithUWjwIl5XsafXPZkqi1GTK+ZjDH 2a25bIlIu7ImC2Ptxp5zI4BRn6/Ha2qLY5LZ2nXaHaNxmVPjmb+B0k6rAyq6hAK+s2YV5pIOjLf2 ClQp2pySOTbt9pG+C9qf8l2gjoGmGJPYjLGuNRaA0Xct2vUWm8cVtusNCIRZaBC8LxUiKgBSbSHI tmDBZR+e6lZwnsVZmMt/u1yHwrAMZI9ACPkMuLwBBgvMR8DR0Rznl2d48eIG1y+ucXQ8Rxd7tF2L PkaVragCRwRyXkOjSxsdWwRj7Xmtg6vqZABiQLeNz1jkt2oE6/lSbzHszCcg85TJZ+gJLUjVd/Xh duZvPT5lTHIZeV6XOth9jsp4OK2TgQv77X/xy1/8qk4YRiSGVKaRYGY0TZMJhYGLf/zjH9k+48OH D1itVlgsFpjP50+q/Q+BC9u3HHfNDran4VlTtcm2CO08k93AWLP1gXS/UnMm9GpPoaCCY4RLxfo7 Y2XmvGUzlmzIJIDqzd+XqQ3LoMnf4/fYtgg5vcMsv1WLkReQd4D36kMvkRfhCX42A5FkQDU0nmQW IVbAwhaHh+Qx8eoNEkm2PoL3MtFY1HZOywrkEEhyJoDI8pBlQm9tcQowxPbFQFMNMBRYKNhIzlSz 4x3DH2Yc6nKeU94hUPnUcwU0AsV1uVhiN+Sy5iL0PeJmg/b+Ee3qAa7rgCjGxgIaDGSotsFcSStt RK/AQgBGBRpSSTjGNYDgKc3E8JPjVVRhwrN6eazR0HWU7Fr9rf1Stl12A2U95xg/sxfMj4hpff8z 3zQg2mOdg20FTV03OjZgASycrfAqY1y7W2x1X2XxvGJytZ5nPFYM1U4mgPuIbrvFZrNF27YSDJCB 4Bv4EEQQUePYpGMCIlgG36Gd3HiMxi1UUFFfoXwF44NVNWU8qut7EWqdU7faHgkRR4sj3Nzc4ObF NU5OFkjg3J4UI2TLNqg2gws/H2uJeKQdHTcFo3nIo8t7gMhBAaX+e/K+54OLGshM9OYAFBfdcU2H aDCfDTMXzQWpLbFpLqRc/9Of/exXWS1mlstUfgPIVs0GNBaLBWazGQBgs9ng/v4ebdvi+vo6nx80 avSpF/kA1NgixpQ+QruCAFBSg5vcI8UThGQJEbiE1yVVsydhgpRItj46Cc+dWk0i1kcgJrjIwjDh cge6wULVdkwNLJV4Dj80Q6vBhSFY+e0G99qeLYBsQW7Ax0HABRGBQgBUHRg5oe06pJgwYw/qkxjl OAf2DtEBnWP0YE1WITYqnhw8u9zXpGMRiOQaObhEuq3i5TeK6syMhU3DwUhgBQmmhTAr/KGEo/1B BJHwOSdjc/yfBy6eU954XtfPPKseeTxNY8FK+KVdgRyCkxDtPiZQ12Jzd4fV7S1426IhFnDB4sbH ldYiccr5JfpUck30qqWI1TZHHIGQsb3FeIukfMwLxLQctnSGwCKBsuHntBai1lLU4AKZag/uQabj g7EYb68cGgehQ6NCMAQY9fjW1/K72ORzaXjeRsjf1XVrg7ZJmlXmvbTdDAft/kyG8j1W4WyXkvu4 xha1gSjEm0RpY6UyqtYXZKukbbFarbHdtvp2WeekBtuo3g8gS/XsRtobo9Pq2UJGM0bP5344sFRY aQ2R0DbfSMyPPvZgJIRZI/E9UoQPATcvbvDq1SssFgv0KaKLHfo+IqaYGb+BPjFeLCMw1kyUSmZk kccxa2aqOWYMeKo9Q8ZefZ4AIFK/8bZ++WdlO7L0EruAhTDcIQChenqXz1TDU/oeqICFjSnDEYvm YrylwWr8Yy+w80SyNXJ8fIzLy0vc3NxguVwihICjoyN89tlnaJpmZ/FNgYuBxD9ZewUZ1XcGESOL k2EUSVFpSccJMbbU4I4JLkFBRY/U9gVUcCrpjW3QTEORBUeZUCY9CZCQDjb31gTGzmhUx7hvnnPs 11zYq4Z97RhZRZnBxWBxkHpxeIAA1wSACF3fA31C97ACb3ux+vUe8A7Jm6ageo9uE5kGwnRlDiWw Clj7nyTYkwEbAxahCZjNGrgcOYjRc8pbHdVqQ7VRootVtqusv2XM6+U11Yc/zPEpmotPfvcIXNi3 U1fbQISGCb5PcH0EtlusP95i8/CIGQBi0bxxKqrvlFLRWKi3RzJtBe+ChN28ILuain2aiwwm6vMT GovBfVVrU3W/HfVzY43CWHtB2KUx42Pf1kgGAp94TNVPRq6ApnLO6MCu8DRCRqP2Hagz15L3uE8Y U41iC5A2Al/I42IAQpj2er3G6nGF7bZVV1ova5oqIczW3oBG10zZ+EHtPj9B/8d9UV+2tPamMan6 wOzMImtQQ+8QmgZNmOF4scD1yxd48+YNlssl+r7DerOSaKCWp0Cji5pqv9SVMpApWgAq9TTvEoII TqCd9V+T6p05+wSYqJ8pjH1aeD+sqcDgej0xxgDqueCCqr4qAbUY/utffP2relGYaqPulKnKee8R QsDp6Smur68HWgt7LqWE+/t7/Ou//ivW6zWcc5jP57CooPV9A4lCmZhzkirYaaY7cg5930PUcQRN H5gbJoWxSNRw6uetKreec5rzftOC+yhpi9VYkSz4FautACDMElSYHBRMOMqhtDlvzFWfA8e4T/dN DNMmDT9uekyIrSsAAjxkP4StDTQMiGOaKZAADDILcpKMor5L4E0rcRE4wXsHHxwceThn2V6TakEE q8fESA5wwWX3LsPG5KxyTqzotf+QtRUM8gTfBDTzIHE1SNN3G1MyQqyIxWwubF07MEICEIvHSt2/ 9fGp23bPfc6O8diNy6i1dFOHSfLWP9lolSWGyMwFoO8xh0OIEY8f3uHhwwf4mBDAcBp+22JPmEsp V7YU2RajPm/2Fr25mZZzljcmJWiMC9v3l3HJGgpl7HUsm7LlwXovBsBi0HbVliYq94rqHVnCr+0U 8pxQwm7Sp0n29qk1G9kupPrYGrHaTI3dQBI9sI7rNrNWZHyebLFWtkTVLMk6jYRK4teGWFQBE7YG ACWDjoqZo9KCwPK42OatwoGBWx1VdQBS5QIdezGQ3GzWiF0vQoMX93bHKpBZbVSzIWWoJ6GXSc1s msnqlVrbAc+2vhr0b3kg92t+wH4rvdRFxBC6M5vNcHZ+hqvLS5ycngAEbLZrtNs1mHuQg7rEi5aP vCveOlyJN8qjck8LeTO9ThGCxoy6EKa9NOKp8wB2PHEGAieKRmLf8+ChXc+UIDQFLkj5X21zUYML wRCKEb7+xde/knflZbVnIQwPu2ZWugYYbBCcxsX4+PEjfvOb3+D9+/d49+4dPn78mL1Pjo+PwVyM cuw5OA0xrL72BipyDhQgqxmtM2WvT1TujSJHp+nNY9sLo1TvD5fUWDFR9gBx0NTHeY0NER3nCe4G BqOlAuVzGF4Mj6m+3Y9e94O98X2kxNLAxQBlZhTuFbEb0pYgYA2TBLZKCbFr0W9bpC6icQHHzUwi kbJMMnIEOOmTZJwegKALlwEXO9l+Ia/3O2E15AguELyXZE3BAyfBI8QExB6k0flkd5eE6diWCcl8 cxA7Ds2JmBf+dz2+r5ZjvH4+uXwbLw2sBGb1HXfwTPAJCAzMmNCvVrh/9w7d6hEBhEAs/uUDN1Bl DEnCeA+8QAYxKjhnPN3RPNTncFjyt2MsWceqHFTl1/czoInxhtsk42N8rdYQ7Lquj57dQ9ee0ogM 6nmANo7LK9/DZ2qaa3jA6j/17p03KdC2Mms7sHH9mA2u6Dtg5MrQSdVmMgCjvVqBFUkC7xD7HuvH Fdr1GhwTAjk0TrY9iajYzmidvPcAle0ykMXQsJKVLU8KT7snBtohGt9jJ7L0Yk/BhNHZvMHF5Tle vXqJy6tzuEBYb9dYbx6FpniXyyzhFoL2r763GqJcHeZsfF6u7dLn5x57BXwcACF73lCABUqf0PDa c8DFAFjk+6DCrwEQLuAiFz5RqSkVztQxdV9KCW3bou97PGqQrru7O3jvcXl5OQiB2/c94Bz6GFVl L5OSiND2PYKXqBKcDFDQyItAJG90CegkNHdse7GpiAnUa9Ar1q2DIhzqxxYWkKg4OTIVjUVeiwfA hKsGfeqzrx+nJsqoh3c+RPV5GWDWrYIc68Kp2rKydRBpwKmRpxhbm6U2qKj+uI/AukVab0FtBPU9 GucRHMH5gEgM5oRIon0AqTklia2GAAsgOYA9AZ5AChC8I4SZg4TVYHjHOAbjkh0+O13i8+srXJ2c wKeEtl2jjx2cF4ABL2moTSqHjo9IQ3wQXBwam+cAi6eef0od/9Q7XO7/PAM16p2TuCp9wpwcXBdx /+49Hj9+ALoWjXMIBHhlTkbIx9qJYmcRdwJiRTZth36j9gTRDKhVdlPLG1I8Rcq/WusgniZiLG6E zSQ/Y3q2nhiaeIoVIBuarz7Me84DmRYMVvZYlWGslSomVN07fb8x7/o6chlFSzGcK+Xbyt8FVfJt fD53BGoYMNBm5A9lmgh91rQ3VvYOIARMOiuanmoMRWs0ek+pnKY5lzDdfdtj/bDC6uER3Ec0ISCE IHYQ5EtsFQgdstGpD9NIyVAXgTHT2Z2jjJ2A72qE9L+yQUP18ClkEs1oMws4PTvBi5fXePnqBc7O ThEaL54lbQcAorFV/lTWrv2tzHkw3sh0dppRVwz+mRijaD1QPGfqsjHiOfvKGfX8/nk6/B7fp3Ld 0BWVSj3N22kALmqVyfc5bDCcczg+Ps7bJqenp2iaBn3f4/z8HDc3N/mZGKO4sxLgGzHG2W63AIAQ SrS4btsWN1Lzp4WADEQG+oh+swW3vcSn6IWw+oRsYGgRpK3PSqeU7/rveoKzSsb1MRgw3g++8j1T aPOZzxwCKgWFQlSSynBdRuwSfY50jIwhu0FbCUkVDo4d5uQQEgNtRNpssHlYZ7VXaGay/6l1ME8G MlBB0O0Pe790vPcEooiZBxowXOrRpB6LxuNmfoQvF0t8fnqKN1dXeHFxhrOTIwRPSLFD262RUp9d NCVnAmXCjFF4+h/ymEL336eMfWCm7EerdpCUXDLBM6EBITDQ3j3i7ttv0T7cw4MFXEAyZHKdxrza /qg1FznV+U6cCmM2o/ODvy2jKT/7ngwKSkNzK2tJO1WaDVTX6mNKkKnLoIl7ntI2HNI4lWeGQbPs 3O4xbdRbrk23o566Ra8xvHf/MzX95p1nBoBmBECyJglQjzsUY9BBefKfwCkCUkLftthut2g3W2zX W0QVDmfNDN6H7HVk7G2gdeAq8BZV66Nu+OjYjfMzZLSmoSidUwCLSZQSqFMAkvcOy+Uprm4u8fLF C5ycniClhPV2g227BRHBB5/ztGQwUxnK53MKLmrgOb3GhwDpeQLOriYBKPZt+bNvfqMAjL39V/V/ mb/Dehu4oPq9Fbgwg1j/9S++/tVwcOgguJhCY08ds9kMR0dHODs7y4G6Li4ucHx8nNWzAOCDx1// 9je8//ge3jvM50eYzWbis9/1CD6g8Y1qHvSTCBQZqUvZUJO7JEBDV0gdAGqcSXOn9zAyEK2BBQ3v zSiyRs7YP7j7jkP3H+rjvSDDeRXGqr1/WPCZ6jk11jKgARJGTV7SO1MSFXtg2Yf0ypy26w0Sx+zl 0XhJnuVJoufJFFLSRJCwu04jdXLCjIDAESFGzFKPBYDzecCr5Qk+Xy7x5ekZzp3DESUsmoCL02Oc HR9j5qUesdsixg4x9nDewWna6B6kVujTIcCfexzSLnxXzcahcsbXZXOvluLUjZcdAgMNOcTVGvfv PmB1ewvX95g5QuNIwq4bswAyuBh+F+1EtpuopeYcUhoZkJTfqCJtym/TnGdtw4iRCUM2IKHgo2Ku zJbQS1dRxcDsu/5MnasJ3fg4pEl6au2Nfz9vrT7nfZ/uUVRK3QUrBgQyc9X7iLLPw9BWpdJgZGCu HZgDDypfLhYbAFi2IaHziJjgnZetki7icbXCerNB7HuhtySxI8Qt3WcNi+RtGo5YLVkf3tbc1X6M 11pmovZRbSmI4SrPNBOqEjOC91ien+Hm5gZnlxcITYO2bbFeb5BSlG15cnkgiIrhp42A/K7j8uQK Duv3xDjzFP2g4cgLwHv+MZwbw77L16maPRN1nAIXrv5NZrdJ4i1yCD0PC91PEAePV8S5dmUzg07L vGrXzO5ivV7jf/7P/4W//uU/8O37b7FarXPisKNmBkceiLrfHxncR8S2QzQtRRuBPknaaeaMknM0 zol21YaZ2TjKEH3d8ToJ82+bYPrtBqq557O2fYznOaBi6n4xfFVX00o1Z0gZKAaHIMqxL2zG5DIY 8JBFaXYp3nuExqPvesS+R7ttkdpO4i54n0NRk3c5UI0jdUlVidpzjyZFHFHCqQ+4OT7G6+Up3ixP 8erkBFezGY4h1z0xHCV4jph5wtnRHNfnSxwFD8dAbNscBIgBSdbkPIjTxBL69OP7aCimyhoTl33v svw0poGpNReBAdcnrD58xP37D0ibDWYEARao7A1MErVPqr/VnmmUk8J+2/e4jPq3CYS1LQag35XR Q3nOmNiufQaPOMlTNg/1fVP9+dRzzz2eAzCn38WDuo2fm0pbPfW+6ffT4HtHOwMTEHUtW8fbVSr3 aU2H5N40TfXtnPI4EpA9TMxLRLa0o+bycIh9xHazxXa9Qde2ICLMZnMNtDhqY5akq1ZNgPPh+qGC pPb0UNEdMEqyQ5mbMgfF/giEHLnTqbFpmM1wfn6OFy9vcHV1haZp0HU9urZF7PosqA0qoOvC3DGR 43bI/wOoXY+53TXFB6o3EBWtj9w6HQ+qfsdUv43v3+EjE2XsljsC9NXvQvMB//UvfvGrnQIwVbHd Cj0llUmyGJ/PDQiVHnaPcw5t2+LDRwnI9XD/gPvbO7z79h1WjyucLc8wD43knTBXUo1TwV0C1J6C RFwr7YAmxrK22L68fmoX1xJbASiqt8J0CxJWhIbiZzy2szi0QKYG9lB/7uvfyTLUQJNpVE+iqrYF WMifprmQhedY8pV6pwa02j9J6xecE21S2yFuW6S+l6BjUCkmqJspSTniCsyYIeHIOZz4gOvFMT47 W+LN8hRvTk7wYn6EM3II3INdjxgSEkUAPRwi5kQ4doSTJuBqscRyvsCMHFLPEgAtcZaUstT7PY8f ClwcUrOP32HbiUViLMTSMcFFoH1c4fbbd9je3iOkhBkkUZxXjkAo2xNQQFF/l0BYmhod5VzUc7Bz KODBJFoDCPlaPq+VHanl7SdrmbvMObMBiaj7DHBRX3uK2R+iZfuO54CV/ds1w/O779xlC4P3odgO TdU3l76nn0iv5W0Ovad4jBig4PLPQKKVm0qwwDww2rah4Gj2N6R0RO6LUQDGZrUWppyNIkdbGijM ybzMMgAq5Hei/4bP76RxUCBRXlVAr62x0MzATBK1k0k0K1oF7z0WJwvcXN/g5YsXOFuewzsnAlUv uU1qOkP2CuuHMWDYIxDmZ6k6Zx+M2l71BVX350+BKlX8Eb3fhN/BM8MeneIrzwUXgzgX0NwiP/9a tkWI1P3RKvfEInwKWFhMewvAZQDCDrNKhiMNNyzZ+F7cvMDlxSUW82M4eDzc3oMY+NHbLxCcx/ph Be4VVERF0HlqSTc5N+z0bNqTUWU1auNvUBWP04CIqQSrtnPRVoxVUyb9P3U8T1LZ/8zkwANZp5j7 JfdFpcnI/VQ8OmzyBudVWlHLWTWuzbNRVaPBBXgipLZHv91K8DFH8E40FQ00NDX3mANYBo/zJuDV 2RKfXZ7jzfkZzpuAoxgxSyxMkhJaF5F8kgUJhktAAMMnhovAzHmcL05xcXICD0K/bcFdB4akdq+1 NN/n+K7g4jmS8yGA7shJ2nsYSFbAlyTPy/rDLR7ffQCvt5gTwacI4qRSZAJBvsEowKKqExcuUoAB 6a9U5vre/frqd1avj9ovH0PuRStY6EMpw+amMcFPARd1P+67f9zX+2wxpu6f0qI8Va8x4a7LHP7e AzpsrfETs3ifW4x05O64oIY9PLgw3Cap7uRqPJVDMTR8gEV2pqp8FgNea13sI9arDVarFfquy96F 3vkBSRlUpmKs0+3fTzdJaUbeGqUMcwsT15PJtDFKAxll7JhME+EkPsb1Fc4vzuG8x3qzwbbdgGE8 jpRWFVs2VqRt/VO2zne1AxmZ6OcQD6bquR3aQfYcMpgZajz2z6YpcLGPv0+Biwx2QCAz6PzF11// KjNN7RDODva17DQaaqcDtJMJlVWNa4uy7L3nNLYVsxOvDOsZh8Y3WB4v8fLyBjcX17g8v8TV2SWO mznQRfRtD46CPLdti7vHR/jGwzVevBHyWAnyDj4UYmXBwVR95cmBU0LjPGIUVTprFVmzkZm/fRk8 C5Gba229nruG3fQgZnTNxSBq0LtGTPYO6pBI7pOuLLy5CRIZ05pkW22L1AQoB8MRMTk/I8hUPU0A cEIhDgwEJoTE4HWLbr1RN98ex0Q4JmAZCC8XR/jRzRm+fHGJV6dznM88jiihQRKp22Sq7MJDxRuI fRFj4MCU4IhxFDxeLE9weTTHnBKAFtvNGuQDou3tgCuCXw4WHgAAIABJREFUr+mU2UIHF00aWWPc /oVdjQbGHgbDz9PgworJHhPGfCEGm3mxq8YhkIdPjPi4wof/+AvwuMIsJjQpIZAuR0cav4AVWEjB Fg1SNAepYgLF7TRlhsTgWOJQ5OtyWaVbJd/6O+b77LyGi2dCRBaExQQqS9Au103qVRhcZm0GRCY+ qOav9fbgnh2pfeK+0fvsk0F6PVSj9bd3SDN4QmYwNufqOVJ+y1aCPGvGAEPaUoCIaX+lFTYWmWVV gCWpgMNKw5O9KwcZMSZrcSh0riijK/1Rg08bXxkv1HPXAMhgSw06D2X7u9tusbp/QL/t4AiYNY14 lgBIHDNzF42A8JGqZ3XZKfO0bJFGOKs974qj6DcN+rBm8ENmXwCV0D3K9CjMA06WC7x48xIv39xg vphj022w2q7E883id9TlOgX7LPTSDnPXFcNHq+tgdmfCoKOePzIfUMW5sBhRCkwymBry5MzjiYbv Kb1bzW+7d3hfLm8CXOSZSGof5gD/9dfiLTKsTFkg04dIrmPVZEH6YklpiFGn8W5zNOiKARkJDe3h e+SQ0g15zEIjeSvU2MxBosN9+PgBv/+3P+D9xw/Ydi38rBENCRPmYYa+7+AgBM1RcWvNkx/QfTch cF7tBsZtMkJmah/L/ll7WQAKKg4RHh6VOzpcLc1QuW+flDt1zbYmysCTLtKCok1jQY4ymBGwRRq5 0xCvEgrNqEQQrU4IXlTnKWEWAubegWMH1/VovMPtx3fwSLg6OcbnN5f48csbvL25wMvTY5zPA+aI mHFE4AjHSWmiMJ2UQ++SBc4AsQexLl4CfCNppV0fxeBzcYTl6QKL0zmo8bjbtOhUM5ZDnVMVCIdK 6unc986BIV4UJYX1vuOp688DF0y7jIoAIKZCPHTuNhAjzsdv32Pz/ha+69AkhleNhQQmE2YgbtY2 r4fM1DxBiiq8cjnM3iEGIioGPGb8VUszHbB+HryzYups4N3tPjvum2dof556JkvjNQgf1X3qmAIX n3Ic0i5OXadq/56UURfiS6DBfBwzgJoG0ahNw3PDfB9WtoFEzuUBRWtmAKN2S536f9TCisnqoQgm dj22mw3W6zVi1wFIGpBRAvSlJDDIvAPZpD0DTwrIMxin+iU0ylFS/uXcUjCmXdHt3d61OxXQyMc3 Ds1RwPL8DK9ev8L1i2s4T3h4fMR6vYFzhCZUWpkRhOEceLAeTwUGuYmlnvtArMvnefA95N/TQHif NmQXOA/LHMxZGEusNBewuYjK5mIKXNje2uRH3psnKg+GQtRJ0AlBTqVkdXvMFrSi9q0ngYXmRp+Q 2h7ddou+7UBJcimQ1ss7cSdt2xYfP3zA+/fvsdo+4v7xHncfbnF/d4/gHBaLY3SxV99qlbyI0PUR vgkZcTvvBTEHYULFuwSVKqv6cLW/NGLsY2+SPBlGi7oGDVNaCAMxzu7F6DfbnKfcf7aABDWSal4g 2gvTBJiLqRqxVrhJtE1e3mIp6R0cvFp6B80B4hyQYodmFuAdwLED0MM5RnAJ5BJuXlzizesb/NPb V/ji9Utcn5/iZO4RKIFSDw/NesoWHdCAQ4kMqzkitUc0TbLmlOnUJoCiEGEiQnN0hPnyBO74BOsE 9InQd52ACbACqpoBFeZaDFxl6ruJMRweh6/nhffEOGcmVtWLAAHQFn6fgZkPoD7h8f1H3P3jW6T1 Fj4l+KT9qGMrMJ5LMDFUEiVbrAllF4wMMPJ9UJW2SZzP2ArIycnG13n/M1NlPgdMjLdC9j13yMZl X7mDMqvvT/oosKpBHarzU+cwOqcVGsyh6Tbs2mNk8Fy1aXf7irJ2tmhZeDBPMJgX9hm6JWdtMMb3 F3BqEhxn1ZXY+fR9j7YV19W+FwNJ773GyGgy4MkM0jkNajVyNzbmNqDTZfsj45FstI4hfabyNcAo GOozJAIy5XJCCDiaH+NseYbrq2ucnZ0DRNhs1thuNhKsLv+TdSfCbcjgO28vTNTBqL3Q9CFIqhl5 PS8MFFq5Q3Cxbw4hA9NxmeN7PxVcuL3g4gnJ62Ash/ymynCHcsR51WBRzj/hE0ARQBeR2k4yk2rw K0Rxd3RECmh0AkdxGzo6OsLJyQlmTYPtZov1eoWua7GYH2N5utRXkwAMr3tqXrKW9ikhcoTzDikx Qgiq1pOEXTWYqAfe8mSM+8zcV227aNAlO120n/FYeYMAJaPf+8oQCdZlibgGPcK3tXxnE7uKk08F nHgKkuQNmtwIwogsAE0IBEKP4BNmc4/ggRAYL19c4mc/+xJfff0T/Pjta7x5eYnT4xkCeiB28JzQ OEJOx1wh+/JdgG1eIFJzGJLtUpR8AbpN08eEBII7ngHzI/DsGD2RZHIEIyX1VlLAa/ZAwIgREWVj ysPHU+DjeVJ3rWasS5Y579TVD2jg0K9WePjmPda3H+FTEvdgSIIgoSYZEagrKg+YQR3Lwgj+2J4g E+4qS239PWhhzWjG7Rk9O0W0DpU9LuOHOqaY8aHr37X8Q+XsXMtzQBk9Cgmt6zm2f5kCZs/tL3vT VFnItajeO3qXgE8DIvVKrZ+mUVn6TgXOzIy27bBZb7FerbFZb5D6lLOUZrqtcyVDozHgGkCC6ZU5 kOgnAMpu/xAGQbAqEABAcvXEhBAClqdLXF/f4ObmGovFAiBC13douxYgIDSSHDLHfxnVeee9IBBp dqtRO3ew0Q4PGJU2CUCsp6o+qPqIK2A73Ycj11OtkwEK41WOaAJcCNQawOzcMJRC96ozCbLPiqwA gcXP90k9FxKBEoH6BHQJ3EqIbtZEYubtISG5Kyaf5N1GfJ2TEOKX5xe4vrrG4ugY8zDDzfU1FscL kIYRJ+9AaohjuTRckG8fPGKKIFXJOYi7X2nrEFxYPw0WN8qo20IbuKaOBmlyQo/Axb57xt9TAAOE YQRRu1+T89SoU/i1eVrIODWukQeTSMHOOzgCggN8IDD38E4SeYQm4dXLK3z19T/hFz//Z/zox6/w 6sUFThcNjjzDU4SnhADAE7Jrcb0SOHey1i9PPgMXlFEeE0BB+ylF1awEIHhwE9AS8NAzeiIcNTPM Zg0YQLftkGIv8yoHDnODRVXAxWC2Aztg4bAM+xwiP5YETAMo4yTEFzEhgOC7Dqt3H3H/zTvE1aby wuEy9mANy44cPrpIkal+c7UwtTWVtJqZR762y7gO/b0PsDz374O05TuCjXpdferYfGr5zy1nv0ai MP/DYGjEIMZ0g0YccViSaq0OXq1+D+vNo3N2j4EK0UbqkjUNdV4elQsli1dJuxWvkna9Rd/2IO8R QgPvApzzAoZVkWlOB0a3oJoKx7UvTCWi0Lh+E7QW9U210KP9SaVPGAlwkk/Fq0Dqg0SbfvnyBa6u rzCbz7DZbrBarzQrq4cP6jXJogWxNUg2ViDtK1fouNW1rnP+pzXfAQ2Uz9fPD9qD4Xob8/SpqTMu jzCuG4qAq2Aj4KmDMZho9Yvs70xUqB6q0gmiqRAJGInFyyMmcB/B6vWBpOprhkjH3oOgatck+85M BI4JkRk+hAwwUow4XSxwPJ+L8ZsOuvVUAtBtNvj3P/0JDMbV1RVOFieYz2Yau6EBNIpcIJsE0En1 vIMYOT5EHU/jKUL1HKK07/cOis+/eec5IpI2Of2m2nraQJQwlhR7gC3XhzQqcQdmGcujmYOfOZyd neLzt6/wxY+/wMXlOY6OZpjNApg6FElapyKXBHXeBWTP86ppiVJhjmTnoC6W0HgkMs9ikoyOs0BC gLxDywmpj+C+g0fC0dyjaRZ5S2f1uMK27dQjwjQ8Qw8m2fP9lNA0n35Mjjkbu9A6JJZcIczoVxts Pt4iPq7gU5IInNo3RAyvk1XKIDDiXmlU/hyDiyEoSigA4ylAUGsmDgGLQ2V8yvVPPQ6Bin1al089 prQz+95p76ql52lwxYN79gGYcbmH3pvrZRouFcnHXnxGD+wPKVfsnGpGZ7G9hf67vB3CTlzPYVpA Jg37X3iDS0JfXJB0BYkj1vdr9G3EZrvF6dU5lmenWCxO0DQBHERvyillfpRbaXTa5qx+rLumesP6 mYwAWgGkWzKTYyqL1GJ7xNQDrgQRW5wt8KOjL7A8W+Ls4hx/+MMf8Nf/+Cvu7+7BSY3iNTKovLaq g7ZjaiyH9Sjzg3R8xltg4zIEbEy0fS+f2c/vn3fIWE2ACwZT2j1dqolBLwwfVbsAkf4IGlQpiZVy iuK+xJ2AC0QBFhakydQEed83v1MOBwDOwYeAru/F02M+R1+pm3zwChQi4Ekzq7J4ljzc4WH7iHd3 73E6P8HJ8QLn5+d4eX0Np6HBBcQo/bXJScP2SVUn+iAj4T099AmDdAhgTBGa4fXiUgwUmDfE6ZIM TJQC0ioJqc4AJFmY5ArpBRSgx6yZ4WjR4OLyAj/+yVu8/eJznJ6dYDYPcAQkimB0iH0L74R4gB2i WoN550C+QZXsFLnDYKBMvY/YZRuM0i5GUkISGofGBXgXsiuDA0ApgUgMIpkjiBwWxw18WGI2a7Ba rbHebBGjXAczyhYeDdHOs4+xLPe8MgYGdhVhJGKQY4RE4G2H1Yc7bG4f4PqExjQPJNt7RJI7BArM pYwxObU9bAV3sP3+Yi9hBEtO6HWuSqr+ztKr1qN+mzEn+3t8jM9NMfhP3SrZJ+HvaBifKPdTtBvj 939XoFLXtf49jNRiRs6jdZ5XMutWYyFYslumz5lWbljrKvNs0qibKoFGkUDZJbATypESw7OTQFmu SNfJSRZex676eFGNpySB7bycsyzVKUXVHgIUJYInBUJMPfo2Yr3d4uHhAY/nZzi/vMDpcomj4yN4 73SuA9mxxnoogx35PWWUPZ4L+2ioJBXenacGWEw3Qs5pzp6IRJLsrDlqcPXqCsvzJX7yz1/i337/ B/zv/+9f8fe//wPb1RbMSemzjpFYYct41+/kMsL6J4YXC3IimAEuTax9VOCptJMm+mfYH98P2DMm bC6EaKQDC4Qy4ioVtd+qak6SA6GOpNm3Hbp2i37bI/VRczer0SAwiNFeM2HSCVn/jjHm+iUwuq6T rKwkyCSoEWJMKSPTbLQHwma1wXazxcP6Du16i4uzCzQ+aAxiQfM5Yqcm3dKmD1VUIw1Cvlb31jOk jqc0E8/9PaxL2eYothRSN6/3lN/SdYG8ZLOTON0AMcgnhMbh8uoc//RPP8LPf/FTfP3Lr/D2R59j eb6A84BzDAoEOHFvbIL4wFqAHUcWNtcJcKSyaHLNSY00q3OsQM8xZKMPrO51CY0PMi+iqHfJe3Te 4a7d4uOmQwcx1nUkxJMImM9mQqScFxBracFJAJkR9N2RemqhjZH+MOX6+GNHnlY8LIo84Fm0Fu3d A+7+9g22t7cIZtzMOjY6H51uZRQvkelaplT6WF7FecukSEOFaB3autjpgSzBDsFFPVcPAYtDZe+7 9znP1e8dM/LnlnGoLuO199xyDmksRStABbjZkIw1Qyj8yJif2EgU4GH35XccqF9t4EvVLKnv2HnC xt3qY+UA2S16wDP1JlsjUl3b3nNZa9D3HdaPK6wfHtV1lRB8k+n4jrRfEQ6ikjhsanzqo+7zuuwh 8+XclnFwKkDlkSoGiHiNBCwWx7i6vsTrN69wcXGBlBI2m01OkZAFmiyyj+pbf+e2TY1LNV6TvGDc 5iHv2OUpw/LH8zrXuuZ5NPwGoWguBoW7SjLJBbkc8pRZ0WtVYee8ojESAhcZMfYSojtGxCiui+Zt kaO/GdaqCVnds/m3IXsUTbsxxyDJcQCN1aAPBY2j4ZyAi9evXuH6+hrr1Rr3d3e4/fgBy8Up5s0M gRy61MM1Dm3qEWYztF2Ho+Nj9G2Lvuswb+bo+x4B0kZzb00p5X1yI6i1JLIDxLB7DCa+lTG6vrM4 RsSyMIYCLOw3gcS7gyCaJC9apchJXKc8ZcbErgNTRAgey4szvP38c3zx5Rd4+fIax8dz+MZDdo/k /kQMaPwEkMY2IAJcAJiQyIlnCJEKy7pwVUNGSBXBFDBS2oxspS3p3ZENwsg52xsABS/bX31ESn1F gMTDyDrdw2O5XCKEBs49Yrvt0EXbDlFLelfG7nmHEaL9Gr/6iFHyFDBz9gwhElc1AiTRHhz69Qr3 7z5ge/eAEIGGCBRFqyR9XZhMXV+Nn1UxiJwtYngfAAl0ZfVWDUcaztn6G6hA2UQfEUmsDfu7zmdR oY4JaWzivmHBw/vGQGXEBO1Xduk1ho3CiMftykaDo2GfAv+HAMRzgNCU5LwPfBFknEg98Xafk7vs cafLTOiggnIGyOK4VH1Sb5FaFzEgWY5R0WkSn61EDMcRRBpd2amxvgVG0ey35iHBRGAXkSIjcdEO S1RYD6+aEfQMjlEcxxLELs8DbVzj/bbD6v4BZxfnuLi+xMnZKfxMYvlEhnooQsEYW5icAf+t6bL1 /z6tmQGTnH+Hx9f1bwDkyhZi4gioMMcstmrLiyWOF8e4vL7CZ5+/wa//96/xm3/9LT6+vwWiaZZU 1taYhbZtZUknZTsor3Kdxvvmn8XsqdftqDMwaru1JQPx/Wvf5o04WUzPc1YBZmdbRCKTQXNDuPoJ CbEsMz2jLAM5bJJOjOCtAAlWq1qkBJELy/1VsRqqWju5QqDTDZP/uLomYMg6RCeASlAOLANN4tI3 Cw1Ojxe4PD/H6vISQeNopBglIFQTwOTx/vYj/v7ttzg/P8Pp6RJHR3MJZc4S8jaYRT8bY9L2MMN5 j67rkFJCo4FiauCxf2/MmrdfIzF11Nectto0PkbcCUmNohShax1m6mPepV5nt4NrgMubS7x5/Qqv P3uD169e4uzyHE3w6FIEOY1NR0nd54xBSYArMeit0g6zxTVwStx22+ByGSj2KyTaFcv6B9KdX1J2 qQa+DIdEDn2KaGOUmUakAX2SvUBtd6TsxeIYs/kM6/UGd48rbNYtmCU8sblX1mMzZWPwXQ8yol6D d5CGvgYCnGyH3N5jc/cAdB08mXu02IQMTe5gOE/raKe4+q6NLTH8Xf2f6zNBgPOr9pzfuQ9D47iq qp90HFovdn0IEibeVd8zIc0N3zG8/ilj/kPcu9u3+++ZAj7GZJkLUzFpVBUbui45A4zaGyMxgzTg oN5SrXBhhKTricAgpQMWaEtqLHGLEqlRuAIjJs4CIANIcBraXs8nKsH/mIEExNRj1T2g37bYbrY4 X1/g9PwUR6cLzOYNIhK61INhwaooR6a1YxhbouqriT4s16E0XvkUSgA6ogLaBsNECm9YNDcJBHjG 8ckRPv/R51icLPDy1Sv88Q9/xL/97g+4v72XVeIEjJDFfmJIvhaGRrfeZ5szAqvVWNb1qncbxkcR O3b7ZrJf6g6aAMT2bKhPyosKqrVJSBaKmEWqqgNkSycyYi9AgvoIbHrpIMudbP1O04wFQIkRQRWS qq4N1LWKzop0Uu4Tuql1VY5EabhfFpnRkMNycQqvSDNCbAJSStikFt9+eIc///1P+Nu7BmenZ7g8 u8DZ6SnOl0sE5yStMLEG5vLZK8U70WT44OHJK9NIuV2Ji1FU1foy0a37MRzw8e983p6zsYKILXI/ ZyMiR6GKbucQY6dACGCf4B3h+HiB5eUZfvSzL3BxLdkBT09OEGbiGhY5ytYHpYEVthwaaUajM+a5 A2C825uqJ/KYutJC8yaRLa16W2yC0YlqAz0D6xjRxiTvZyoqf51QskClXwhAcAFHfIQIAVzttkeM KT9XSwiFFk8txboPkG0p0p7bgu7Vgut5zVlKnMHh4WGF+3cfsX1cSbAsIjV2LmCBbBevAg2sVR5v aVS/KknX1qfZWNg9Qw3Mvu2R/QCjYvTVLbVK99lKoaq8MQAo54rUflijMH2PlVEXPa5ezXynfu9o Yw7gi/rZqT3ySueY60cqDRe4MGQ04/YYfSzChQl3RreLoFMLahapk6C0XjUdxJSjfZJuecMJyHS6 RceUkBKBvJf7nbzDseT8SSwaEe8kFw4zZ2BBjjRRIeWAiezEjsGxAzvG+nGDtuuxWq2wuDvF8uIc y8szzBdz+OAhsm0COQ3yWPXPIbA8NT4iPBvPIIjBqtAUCUS3K4AMyhDXRhGEND5PM2/w8s0LnF2c 4c3nr/DFjz/H73/7B/ztP/6Gh/tH8ZQhhV3MCI0ApcQqpAMa+LBm6koLBnx+vxC6U082rdWY23yq CLB7DGwucgUsIE+BbEKcofv05l6UGKlPiK2kRI9tB7RRwz8XiSzvY+2rhUW2HK7vnYNG9wBDsEL1 OVOlj8sAcnIrBwtrTTlBVx8jXBOwWq/w+LjCdrvFavOIu/s73N59gIPH9cVFJsyseVNitXed3Y/8 cG9f3j2tkRgnQKsDddV7W+MEabv3S24QgoXyVlKkHZUoIXEP8oDzAHmGaxwurs7w1dc/xf/9//x3 /Ognb3H94gqnyxO4hsBISEjqqcG5o4fAolDVRL6c46J1UPKlM6G2PNdrrvSFgE0NJAMDKQV5MWva OHJgCmiJcNe1+Ni22CZCTwRONGCTycArueyLnlg8j0JoZKw4KtMfSgeDOXiIc6CApn22oabxsvG2 FOcO4hnjY8LD+/e4/+Y90nqNGUSbQRxBlKqeRt6eqwO/5fHhside0MPYE2SCiIzOH7KVeEqrMHU8 Rd+fa1+xVxLbI9U/93jOY/vKNhue5z4/Vc4UrRxqN8s7xrSk/h4Al6zRyLBsqlbAYHYpo+Ua4HB+ NGurK6Zd2IUBXq5Qbz1mNQCtIbO6lHKJzVLsM+R330VsNhtsNxt0bQeCwyw0Eh2TilZ2WD4GdHOq 38oDRaCA22W8RhsGPTcYH4Vn1p2q5RC7jYSmCTlfyes3rzGbH2G1esRqu0bfd2Lw6p1qjiJiFNDh LdBjbs9o9CphCFUdC521j42U8dQy38r1PX2DPJUqgbbQS+NBRBW4qA15DBA43auXLJkED1Fbcc/g yIit5PpIXS+ZSaO4EnrTbFhjdudT1RAqY/DEx2Zufc7xxH0TzNsAjoTHli0NC/VtGfsKkmecLE7w 4sUNzk/PQIAE9UrAi+sbHB8d5RTjfd/BhwAQKZgg2RbpewEqziF4rxofDLLElmylJagYRpN/akEc +l0PsHNOM5QCQBRJwjPgEigQZosZXr15iZ//y1f4b//XL/D2yy9wfn0OP/Nq5SmLgcxzBLoFZJE0 7ZOjrbrc9zLhZDtMGJp+ak8kqjzVbSXDZSBkKlbHxW0WTgEDif6MnQO7gC0z3nct7vuIlgmRBNjU OTGSEqw+RqQYdR+TMugL3qEJOoYaEcZCgtc2C7vcw+puZMUAHSYPgoIJcvBUbC2Cc6DI2NzeSZjv u3u4mDBzTjP+qhcPACJTSQ+3Gm0Vy2fMQPR8rbEYqiy0kD2gA+Pbvr9089zj0Ls+1VDzkEGm0ABg QHwPHFNA57mgZgpgCDO1WXnofofBwFd1qDUWk/UZPTc+xGhabpTZbOtTaSQEBHA95wx0V7Q2V2T0 qgJb7N8ICJkokVg9y2Qui4KJFHgwYicxMtr1Bn3bwSXC3EuMHuakMTEogxsiKtE6rc45IpR9dN1P qNhrPkJk+vthGgHrEGuj0EvWCKMV8HJicH50PMfF5QUuLs9lSx6MPkb0XacvVZssdeXlFPMc4SpV Qq0VtDGwOu/OpgKQ9gGu8uyo7RWPKSOFqrwCPnZtLkyiAgliRKbMkp45JvRtBKIQ3RQjSIxfNbW5 lKHpKLI9RVbPHVh4dXyDpw43sTZqYJENmsbXgZydb+DfTdAQ0R6BJH/JbHaM0xcLXF9cot1usVqt cHl+DjAQU0ITAnzTIHLCuw8fELzHyekpZk2D+XwOs8dIKSE0QQ13CnO1ARoHTKnVlAZ4xkBi0K49 E4QcQB4Z8bvg4GYOi+MTvHn7Cp+9fYubV1c4v7jA8WIuhplgUWtCn7Pxh3gQuT1J2fI7WR07kGR7 RsGEBfcSAmRUqa7v0L6HnRqIshqjablRwR85D0uUBeew7Xq0MaEHgXX7g0ksGxNL1t2+F7faqNtk Fp01JX0HJJkSE6Hve/g+om0BQJ5DluB2Wn2wT8YHZ+NjyrYX4itP6Nstbv/xDpvbe1DXwytFitwD kARmzLEqzECCGUbXl3bVwLK+K6lyqn465t9V8t93/NBg5JBGZer8LhOfNqx87rOH6vDdtSYy3jWz 3S3rcH1J19m+PXpAyt8XzYWrby4UKT9n11x1b0oAnGTv5cRmZw2ilOtT2iJPZ82aK3YKTLLtGs3b gyGJ9OABB/UsFFqSYkLb9ehWLdZ3j1h9vMfZxTkWl0scLY/hQxFebX0MxqvqxsE8UIZda2l2GC0U hDBnt9VStq5v1TykKnssiTI7h+L3jcfZ5RKL05/i1Wev8Zc//xW///Xv8ed//wvub+/Qblt9H2v8 IWSD87yFPpIj5F2uzKHqeun//TYYU+0dXCsXKi1K0XjY9QG4sImNurNYtBTcJ6SeAQ2ARRr4yrFu gRBlK/+IKJMLBVzkTjgwqQ0VP0NomlxeDiKpJkXeOfUvD5+ZzWaDbK6+aZCIdb+dMXMeiRNin+CC x9wHHJ/OcDybYz6bwzmHrm2lDCKs1mv87t9+jw49bs5vcHl2jrPlGY6PjjLQspx4zAxfS/41aGAY Z97RTNR9dAhRAkDwHin1si/IDCLGbN7g5PwUp2cn+OrnP8PLz17g8uoKZME4KaoK0CN2sYAvsr1K EmmZSGKUaI9jMF61VkLV9+rW7AaLmiAaCiVYVEfF5MHgWr+YRMkJun2iqlIQIgjbmLDpI3owIsQI KzIQEyNyQp8iogI9IwraPdn7pE8SMTB4ByKfffE779H3vcTGSPXk1DoPxMSnJ29N6I2AEYC+7/Fw d4fHj7fgdQvPwMAOjYCY1ca74Fm7RmLJDAjHbp0OMnoVRfbdwaPvncdHAOf/lOMpo9wxc5i6NgYV +0DGU8fU2q6uDuoxBkLDek6BH72mzFp0asPrRJR+F25VAAAgAElEQVSz91B5SEuVdWi2U+Z9B5Bm Q+Xs8cFQY0wDC0mMGImQg3SxWWGrDQY85QzVKYm9F8gB3iE5yY/sSPKJWPwNMIEigTjBe9EWUCI4 D4ATYurw0N1i87DCyWaFs/4Cx6cLzOezQe4gC8NtkT4NcFjr69GYBnXDbQUAcBDPL9agYqmigxJn KSEmEQiSalQcOTVRM2HeYXlxiq+WP8Nnbz7HH3/3R/z617/Bn/79T9is15Bki/ZapUNcNETC5821 l7KQ/SwAMal1+y7rtgaiPOGKCsrok6Okyk1dRFJXIbBGTGSUbZNKQ8dWFlUol4aDtq+hZb/4ibsn OiwzofGtE6VxEq6i60UmmG5bND4g9f0gcVTsE9zMYx4aIEnsBq9uWO12K8Q8MR7XD2i3W3zz/h9o mgavL1/hi88/x8nJidwXE+bzuRh/whbxiNAwAF9iGBjCt3v22WxQ1kclJHSI1IMcEOYzLJenuHl1 g7dfvsWr1y9xcnaCo8UcFAg9RzgNdBGjRL30bgageAfZOBogc0+NT976mHbNtOyvSdFziX0jvZJV wyTGw5aUKz/PEh/FSGvPCR1HtCkiMaFPDMHBSedwElcvBTDmqikW7ibxk2qaxcYEjuAhIeLJi6sx deJSHVMCcVlIlEcSuaKHNHBMmoFVt4rADJeAdrPFw4dbxM0GIcUcdIgAwDx8NLooiVooA41cthL4 PBQjiTt3czm7U78phvldQMIPCSzGdfo+ZT93m2TqeK625FPe/6lHDS4OFWWGicJwxgBF1ldhqjR4 0iaJCUcZyNLoTr2O7JXlsqceQBrqGqJNhEjdnIReJbY8I2q8aYKnWJFqNE4la6aBSCQhDRzBSQAc 5VeyLpkTPn7zDo+bR1xcXeL8/BzHJws0sxngHJh7DFpt8swALHLVBYV5ccFyWdssuIeyBoPB6qaZ cjymrLUg0oCFnPsCzOjVdT7MGnAEzi6W+Pkvf46blzf4w+9f4/e/+z2++eZbtJutbqHq9oOBncH4 KTSkIrQMxYyylVrowdQmnF369LlqSTH9L77+5a8caXDuBKAH0EbETYu46cA9gyLgEmugHlJ3mdJp RTXBEvWtksbMpqC2rODRR86hBuLPOgpD1QpQATciLe8i0awdGTFpoGaauqekhqZWe4c6yIoiRhDm sxkuzy5wenQKTx7rrsP9+hEBDd68foWGZJvBa5ZY571aEUOkcLDm7hCDWfYszMkJ6JnPZwJ4vM+G nwS1g7HIogrynE/YYo3ZacCLNzf4p6++xC//+9f46S+/wpu3r7C8OEWYecBD05vXSJUAi0eRHdxr 1br1s9pWKPCQunJ+xsZAwrN7WFAbi5MiNhtiVFtG32ZIghh7in0B9MMkjmApRTS+sW1UIDTYMOHj doO77RbrPqIlQpsS+k60FUgyb6GoXggdIcEpowcSm3U9ZSJhXULeSZTB4OGCQ0pR2q1xWzwsbwpk K8kUMdpn5v3CbAS2aHwCeczIAV2Ph3fvcfePb+G2PUIyS3q5TwgvwUJ9mbF1meNcUT8DnJUUC+Tx KQadaXgN5XOIiRIsAm9FC6oP53Kmj30M+RDT/5RjauvwU58vhG33M0hqlfta3a0Z1fXixVIfbENl jH/0yZZIozbsbM1AV40FjYJuaOsAZ7u36plxG0m1iMr9ygwYgbjidYK8jvLUyfSwOpcZsq4lM9oH a/yIAnQVIqtNVPFesHcNGKECpYRirGzbmvmlMSFte2wf19g8rNXoU2ziyHsFV1IXB12/ADyzGFTr mnbW4Iq/5N4y8ogKYDjKXm/E+eFq4FFR79pGC1ljzRDA1cwbLJYnuLq5wuWLKxyfHCNxwrZts40g VMgSTuAk4aSGl/YkNAlKByxG0JBfG5MBTHsieIpzx1O1ljNLBHILzNuyHjPDZAEM8fpICbFLoD7B 9Qx0ERwlfLKhzwEdqg6uJl99PHeBZxyeB+/wc3vVmvbe+se4LhMozdBdvsVRVtkDQx9pgmwLOBIi 6lhAw9XyHGeLU6zal7jrNri9u8XZ/ATHsyOgj9Dtf/QxoksR277D4vQEIXjJFdFG9LFHaDxSjJgv jtF1HRyJjUjXdTgJs8LfGXAaTZOZEbkHwaGZOXz+5U9w9eoSb9++xc2LG0ni5p0yvQl3LAYsFsVY 5Tfou9K7e66NCOCIyCd2uc8M4WcZu3LtsjYl1RLlyHkaVAYpwcOJrUXb4369wd3tHdq2A4WA2HN5 t2on+sTqGa2gls3nvzJMU2IFk7Yqgsq+hDyeH4nLW99KtFmxaodqOgAms1WRpeYgrnlRJ1lKLBJM jAAkj8jm/gGr97dImw2CghbxClCNnEUXyqTYNgF1vCpmYOtnlzmX39lCn9lQEfYd0/PlwH3P2Bra 957vus2wty7/JccELfoe7+fJEofHIG4DGxOHAkrVaCi6NLAwtVYFDA2Hs7bJmQR8hloNQKFssThW ewISg0WhreK6aYEOiTSfiaUmp6SCiINzrFuQUYQmR0g5UaV8PByCbp9E1f4ZYOl7kpgwREjbHv2m w3q1wv3tPc6uLnB+c4Xj5QLkxbVetnFMhqGM8BTeQS28YFsN1h9pH3gmDWvuDGyasF3fP0WDFYQR kLxsM3nnsLxc4uT8FG8+e40vfvQWv/317/DnP/4Jdx/uELsI0nDrkkLDqR0ZRENjgkUGGSOBoxI8 DNqQIgM2DU5ploxvFqorMJEBh9J/FTQDtbIPHWOUysYkBDiVlxvdNwnskFRik+c7H+OZ/kT5k2pf GHraNWraV9bgykBFVsrNWgNFv3bNGGEIActZg4U/xeVyiYYltHlMEnSLvEekhD//5S/4j7//DReX Fzg/P8eLq2sczwO4Y0SBseAobr4zP4P3AcEHEEEAiA+Auob2aMGOcbQ4wstXL3H9+go/+erHWJwt cLo8RdM0GiG1B+CGSd1+oGPX2AkZxGVGJyhWNBDJFTBqBqIVg0+6Hxu8g/iXy1wUT6QAxw59H9H2 LT6uNvjm4x0+PK7Q+QA6OcVx49Eyo0NCzwk9SQj3iOKZAd2OsW/bP2YU4I6qHflvBzTzuXidOI92 26Lv1OCTRF1rjBsgjVpOpZ+UCAYSrZOLCWnT4uHjLVa3d6AuCSkre0UKMjjXy5ah1WtHyuTp7Sg7 ipugnVDSkHOtF/GmgKRxCRNzaArVf+LxQ8zN/2pgUjPh77vlsa/cul6H3iHM/3Abato1ZcsxBLZs cke2owPKt8uIRiRZ0TCK6t5nZiUCrL23CcI4HUi8oAAQMSgltUOAaAAcwOwLAFamnGKPnqMmJPRq w1DcbB1JFmdERuoi+hjRblrJvrrZ4Pz6AouzUzTzGVwISJzQ5boqiGcuUY6T9okuC/OmGQIy0Xhk jUUV5Xf8KeLs9NikKC8MISBGAU+nZyf4yT9/icvLc7x+/QK//fXv8Nc//w2bx022f+n7Dp4k6ikn sRyVMZHd1MRRAB6pkEbIdiJ5/ugarsw7hloN2JZM2V7Jij6dPKqHQojrraiVUpKkTwlZai/5OEZS BQ5P8H3HIank+5Q3/j1YKD/gYrc+ZBbDv4ykNQS4SbBgYDGbwyeAYxQgRwAgbpAfPrzHtx+/wYfH W4S/eVyenuHi7AxfvP4MV2eXktitZ8zCXN2xZO+ujz1mRzOk1CMhwgWHxXKBm5srfPb2Dd68eYPz q3OcnJ+ASQwpzVLZhwAmDPKyfO/+mCCoZvvimAY8KQfdYlGSZIOyPCkpS0W2/cSxPGTSOzHwcHuL h9Ua9w9rvFut8GG1wWMbkZoGbh0xW55JQDQHJA+wYwEXJO92miiPUhGyxzPSeHsmonaTSgDOOQT1 LCE1+Exqe1PUg5QJZ1Y/smwxOgIa50Btj4e7W6w+3iJttwiWRCrXo0I6uZKmdSknn2KIg316Hmku hnfCgp7t3xqp0df42g+75v5PPMb9MjSy/OHf9dR20XgdTgG/gW1WVe74W99gL5K/kqwbu8dYI0GN 5xXQWAQbwPb0UzbGdirROyLEvkfqOIcDsBDiTA7EVRA8fU9itc9QoYMI8InQO/E0MTps72HvNH5M guMA3wQQGO3jGrdth/X9PU4uznB+c4mT8yVo1oCdBPiS7U6payDdWndqh2gAwdqt3eVM8NH1nRyQ KsP8WgCtx6OAjeE4OrXFSKmXYSAZ02YecPPqBufnZ3j16hX+8Ns/4N//8O/49ptvsXncgILQmhTV bg0WIZlhhmbZTEHHrcRMGUhUZRQJutUCFbQqbUUWvEbzjeS6/+qzn/wKfYKLLMnGUDqlBhcFpex3 33LDuu1M7H3HDkjYe+f+Z6beN/58SjlTz9geplk5m4RdT2znHDglBG97UYzGezDEa4GJMV8cSahX AKvHR9yv7rBerXB9eY35bK7o06PxjSDiEOAcxArZMSgwLl5c4Ef/9AV+9vVP8dW//Aw/+epLXLy8 RHPUwIJd5jbk/U5rCB3s5B05dYKY7f2w9JPZAnBFfGxRkgYsq6N8stbLKUrmlMARalPiEbseq9sH 3H77Ht/89W/4+O497j7cY7tp0XdA6hP6bY923aJ9XIO7Hs5L4JlEhAhGb8mHbFHY4sqK4NzgYcUG vJQ1rLi0iYKD90Ha5DRTKfNQ+5EXq0pWzHCJMYND97jC+7/8FeuPdxKJMybtA5Mgi5TgTIORS90F FkIw9jC7vF0x/q7rufspmpjhe8fvztf/k8HFU4z8v+L9+7Snn/LuvbQHu2twr4Z2gr7Z+OyAfqVP 43J3NY9Dm4BKLM1/ly1HfeOg0tXCBsFstLKdhnMg1ThIEDyzd0KOYZP5jmn9UDQSpC8jMxmqNC5g 9QgBQF7eK7mu1MMxJnSbLTarddY6MgMuCAiR4ixQnREtrVNFt4zhu/rDnL3iWNthfZWB2YG5UsCF GLTGFEGO0DRBNaKiGZ0fzXF+cYbrm2tc31zheDFHn1q07QZd10l7XACZbRyMlkiGWgmEmao5Vm1N VLzDgUSgq8bcgMkQXJQhr+8JLqpRiKmHqcwJoHgITGkwBp2iOhRDtVOd95zDAMyz738C1U/d/1yD sp3zisJ9Ngoya2d1b4IZVUrIVhLYDdcEDaXr4T3hanaJi8tzbNoWHz58wLfffIN+2+JscYLj2Rz9 tkc4mkm21xCygeDZ2RLhKODmxRXefvkWX3z5FqdnJ6AgMSzYiQuiWFSLloLZfM4pA6G9+4XP7OMx QdvZPqqKZ+asQhS1arFnqcfBGZHQb08BcEDsErabNW4/3uLu249Y3d0BvWiOwB7BOczh0Gt7Ux+x XT8Amw6ubeHOFvAnx5jNZKH1YJEKoDSvMltA/aeBdR7Wk5gLkbQF1ng0wYN7D+c94not183uiste JgEaypuRti02d3dYf7wHb1vJccOAGeTVvU5Zyskqi4Hmou7PffPb9lqfNokYM7Kyqktb9h/fVYrf J5F/13L+s0HGPpp46P3P0nIYcxyVOaUprN83OFf9X7+zlp73vl7BQu19Z5Y+ZT5Us4JqQKt3qhbS 6ir2ThEREkGYHYmwpeudoPRJQYNpPMi57EQQOMKnYcZhz4SAEhSRlCE6IjSOJNlaTBKDyeZ/YsTY 4qH7gM3jGov7RyyvL3BycYb54liEOtuGSRLuGxqwKtu2sDLYcd9lPML10hyMnY3BPs1gVENrp76n Xd+qoatDcAE9i5b0ZHmM5fInePnqGi9f3+A3v/4t/vKn/8Djxw24le1XIonn45KrXNSLIb3YvMhI 2lnAvINoMIbg3fbWB1mHEKSunnTwwGpQWUkpmvzFVCHWARlhmapk2I9F+zGxhnh8nmj3vmfShH2L l564Z3x+cI92dN1mO5ym/e66tmQENOM/5UiSgCbkbHqJIyJJPhNHGqkzMZoQMJsHnH92gs+uX6Bf t7hangEpIQTJOhrgsek3+O1vf4tmFvDTn/0z/sf/+z/w+edvcLQ8wux4BnhGz624M9l2ApuaURZn qPb/UkrFzmHcJwf6aR/AmDo0XQ94wgdZjLQVhJAZK1IGFaReSX3XYrVa4fb2Hncfb7F5XIHbBOIE lwju/6fuzZokR5I0sU/NAPgd95EZmVU1NTPCEQr5QBEKj12+UnaFfCGXInzhD+byJ5Az0tPXdHXX kZnhtzsAM+WDqhoMCI+IzOrqnl2UZIU7HKcdap+qfqoanZC42oA2BsQ2AlGyr1ZwOB6O2B/3wH4L v5jDn81QVSVcQWhJuC2CZcy/2Jkok21AtRORSZzAJfV+1zOIURQOcAUCj6RoX2PhcZTq7KSIqwjs 12usPz2CmwYlObgo2W2RBECmkdl9WOZobj0gfY/nNsYAQRF11pnnzkk+Lc6+G6oxCaLJgcADQfnl i/rJBfIZC+mXXPMvATBOzYXn3Bc/5/4JfA/ulSttlB3T+627iB3c0xYlX41+79BB/zvZ0pNdVxeW LhX/0y1/ovwYZo3u0OJcRxbehC88irJAUZUoq0qKPJaFlE4oMsCQtGHjMHFaKNmCEZhxjEHqWzmg QIRrapREqJzDyCm5tI0ovMhEbiLiZo/tsUG72aNd7nB2JfkxinGlVk+HoARtaw8yK4TyRWzRtVpC 9vdZC3+mkPa5GIBFcJllwK5hdUZCEEtLoZmE26ZFNa3wd//w97h/+wa/+81v8av/7zf4/g8/Yb/Z o21qgMV174g0TL0f/p+kDem4UX6XtHtn/Xg6z22IUadokXEyCEU3gjq3B1lFJEVfElPsTl84/2uN cWLg5cfnk+Qvub0GLF4CGET0JAuoHVMUhVprlKtCnbkx6kSKYFRFKZ3kHJwXJBpDwLgaoQ0BxBGl LzCqxoAfwQeAvEP0wHq/wuXlJbarDf7f3/wjnHdYH5f4P/6v/4Cbtzc4hiMCWgkH8h4tW4y1JAAr XJn6ocvRT59luThl9u0JtBN+wjQ5bPIDGlSq63KWVc6ub244YjH5Rcl6hY8fPmG32WO1XGK3OyA0 Wmc1EDgGFPCS/tciQKK6Y6K4HGLbwHlCBUa92mO/O8Ktt6jOz1CeTUGVBxwjMCUTKpOAZbG0mKDl 3FDwFGCT3C9wRwclIozGYwE9LgBNizZovH5gUFRNKrTYLpdYf3xEwYyKCFzXSVvJwQJhqEU+h9qR 3CJ5Xw7Rez4HT/d//s2iVOwmeUtk1/wcjfzZ+70AjP4TBhin5sNfaztlHck1TBmL7pl317nYl97p ew6uAaSFaKhu2bhMNx3IUHFzyPyIDLiyRDEZY3E2x3g2RTUZYTKbYjKdoBqPUY5kQXeFRD1YmYY0 FS1BDbTidowIbYvQBjRtg6ZuEJpGfqtbtIcjYivFDJsg4d0j51G5Aj6SciUcEAP29RrN9oDD4wZn l+c4vzrHeDGDH1cg5xFcV6wRJmu6r/Kvl4/nqXKa91E+dvr/5Iq972led1FCoii2cAVJXaQIXFQX mC5muH/7gN//+jv87je/xx//8B0O2x0it5qBGmpekWyqeX/3FApNOpVXRucTnzpgka0Ter0Cilhs fnRjRbXL7GWsyejEwB423nPfP2dLCUqy5zi5nTAVPndOftxL5ldBpF2s9anr2PMhu2a6PgCKQOmk wor3FYxcw5HhQSjIWcwWHANFUYIh9S7IM1xV4OHtA+7v7/F31d8BY+A//j//N27f3uDu4R4tt8K9 sOHLDA/hdYAB53z/HRVNgggvabj5+6bwz0H75u2Xm1pz8IIMmQPiK7X4bwcAUV1KEQBHOEhW0N1q jc1qjR+//wmxadHULUJCJ5JtU8ySEqLaxoA2OrSpYqFoMp4BtBHgCA9GUTdo6wbHukW936E4W6CY jeCrCjUHMUMSITphaqc5ZgqdAQxSC4H+tUVagkIzYeIIVHhUTtwkODZSK0AFxMh7PP74AZuPj+C2 BamF0JFdMddHOX2ycZ2sJfol9SmrRtKr3/K0n6NI6u4Fn0l29nTrxGmPIJppNmnrO+G73Tlas0cc zrNn/Dafu4i/5EL43Ou8JMfyxeA512D+9yXZc+oezwGi4T2eV470+5Br1b+Y4dF+MJB2m40pySRJ Vi5VZUK3WPS6nsVGFgtCdBJ2z0SYLuaYnS8wPVvg7OoSlzfXGM+mKEYFylGFoqq09oYa6hWYmIXF dOtkNbC5xsZ/iokDhxjBTUA8NmgPNY77PZrDEeFwBB8lpD+EoPwIoIiSSdI5j1AH7No1mv0ex80W F7fXWFxdoJhNADgEaMi5cj9iCNJKTqwVASxzX9dmNxgzp7anVgxzm9icpJ6CYw3O2vjGnGAwyAGj 6Qh3D/eYn53j6s0VFv80w29/9WssPzxKriQIh82z02SSqtwBIM1DxCx1qACWQo69cYPOgpVbKtSq TxxtAEqGTkNd9vnpOOwPXHmvzwMNP8f/+Zrv9UWfJvDEnGjnvCRw8v3DRfWlc4ffHQgjJ+6PGIKk OPEevvQpn3xd16jGJRwVYA5oQwPnHeaLBc6uz/Hu79/j5u0tzi8vsDhb4L//n/47/Pv/9X+GLzxG sxJ1fdR3NXBBsHRkvbUpb5TMaPmakMtJqs+1U87FMYKrtVujtVvIUuwygTWkihmgSCi9BzGjPtRY rz5hvVph/bjCfrdHaILgiQARIGpRQNR4dkS0HNAGRsMRgcVy00YSMBICSCMyCs30F4McH49HHHdH VOdzVGcLMS86RnAsCbW0sBEzgzUdvLeJhKHP2gSf+HaZO/Y8gwHn4QqAGq20CuHlHLdb7FZL1PsD XIiwLJ3UVwUBcGexoLy/LWR00DH85MPp7cXF9SkoeP7w4QLajceTwOKZC/ETefL0uF/SOvCXtpie usdzIGcIWl7aTpECP+feT+41sDB0n/VYUjESGcwhWQ8s832hERsxCMgHUbJuO+dQU0A5G+Py/Bxn lxe4ubvF+dUlqskY49kM5agCe5I1RyPHFJtowTBKz2DyvDMbGuJHGibEHaGSGDL3Z6oUhABuA2Lb ojnWaA9HNPsD2v0B7fGIpq7RtBEFJHeOZ0ZsWoTHJTb7HaaPj7i4ucLi8gLlZCTWmBiEuO01Dwcg yqHOY5/bhF7p31NrWcpaygqqVLkC5UnZBtclUTciAVQSZpcTfD1/j/OrBe7f3uJ3//xbfP8vf8Tm 0xptI5mMPREK7wF2CG0U8qZTixNJaH0+FTvLGCtlAjABlQCfgQxi0P/+P/47UYqIdJFCh0wH26li YT9ne3VyfMGxp44hKxQjPybp+OzEzKwOoNw9NLjuiesNr2uDu3AezjvtG0sWQ4CDki8lWQyIcXZx jjdv3uDh/Tvc3F/j4v4co+lIFnFuURSSpT3EqElSdGG3xFe9fhHNXl+i9/zPgbKh22MILF6aFLll K+csRIKSJRlgzSNHTrNOEva7PTafllh9WmGzWuGw2yM0Ldgyvyq4iIrmmYW3whylDgxHNIHRsOSv CJHQBAE9rpVJEYjQOqBBRB0llj0QELwHxiXK+RzlYga3mILHJWqnhdGCsLKtaFj+fkOXUO4GZLMi eI82SGIgIuCwOaDZH1AEh6JlfPrDH7H+/kccl2sUIWIEyw4oWgMpSrQ0yklQsXAtKA77Y+ALpZct EU/PH/azG+zPMnlCsoU+UQBcto/75/ev/cJzpTnbf/4vBRafo0h86fbcfHjOwnDqmOfcKK8pO6f2 vQReOtF3ep4/f17nymCy8FGdtyoXWcclADCsIJcs9s47UFFgdneFu795j9s39zg7P8d0PkM1Hitw UMsEGCEtnE/bKtnV7IP9zkAX3YAnfD/AstZm1UoVi8QYEdsWaOWvWCh2qHcCNsKxQWwaeI4oSWpA VVWFyXSC2WKOxeU55hcLlOMR2JNmWWYlfNpTM8jcvSfGzOe40hRTiOzTRIKdxbkPwonQaz9oHyW6 W2TEusXmcYM//PZf8Ot//Gd8/9332K32CMdWIxNL5Nl1A7eIFITwnhCnXpkt0adxK8RMY6HGknxZ MiyL5UIbxohEp4a6LbivNcwvuT1r9vvM804i9Weudcr8f+oYQ9nDY+2zZ0H2HAM4BslSV0p2zKhm pia2KMsKs/kcl9eX+Prbb/D+q69wcX2J0aQEXECElJp15NBy07kgAuBoWMzWQAYnUs4wOUr+jLkG dOqfHZ/38ykehu0fLrq+KAGtU+LIoaBCTKZ1i7ppsV9vsfz0iOXHRxx2e8Q2JosGgZSkZRaLLi2w 8VkCglgsopphmRAjwayJomVF2Y9OIHoo8IsRdb3HcVsj7o8omwBaTOAmFaiQDHfClelamNEf9zlX tZN/KkAyU2VsAkLTpLTC8Vhjv1qh3YnVohC8Kf5PYsAY9OgybUrforNOvbCxWUBO7E/XeuG30+cN hFcGuNI4iAKD+MQzfok7I/v2Wed8zjVfcif8nOt8zrGnzn3uGl/KC/kcufjngCoigvMeXk32UvBP MUSnkov7E1KUqywrnN9c4/btPa6/foeLd/eYLuYqLxk1Sd0PVhfk8B1IlbaTem2OmxOY6NzCT97U iUnEChYyWEqWFw4oCkjCrxFG8wmqizOEuka9O6De7dHsD2i2O9SHA3wE2qbFcbnGbrfH4XBA3TRY XJ6jmo2B0kPgvuSHkrpP/fb/OWulnCvVk1hzXjD4+UifTOHtmkwUMTCjqErcvrnFfDbD+fk5fvfr 3+O7336Hx49LHLeHVPGZWMnkDvBSEU5/U1cUSSirgD6VC2Y9SlqYKkGeByXXjcj5M+e1afZf2qAn LQTP/P6aST8/7pRQeW5inlpUX5q4p8BICkVVfxiIAM+ILiJSlBoVhcdkMsHDu3d4+OoBV3c3uLi+ wGgyBnuHBgGlAxBZSJ8kTGEikjoZoGdd5GTCnRxY4yBPaXH2/M+5P3pWiBNaluX3yI9l7tJ0t00D B4/SlXDkEJuA3WaD9acltusddus16sMRbR1UoJCQsPRZW44pQyxHIV4yA4EDWha3iAA11nLqhBi0 3yIQo1yjiVFcIb4jI0u0BqNkhgsBcbXDtgYWGrQAACAASURBVG6B9QjlxRzjszmK0Uj4HHWrdVOc mkBdqmkQsyZjKBk0tZ9UeoyR0dZCMCuYEI9H7B8f0W72oLoVIAqCY2NsdMCAMg2R0gS2+50gbZ7o q+f2vf5bHMqqwWb2lBPaFMenE/iLt19WefmllKEvBQGnzj31LC8pO89tvwSoOCkX5IOMOXULMkuE mfNOcz0I34kJKMcVLi/OcPvmHm/fPeD6/g7FYgqMSrCXOSvvTEoo9AqgY1eTQpC7KBWsqN5Zng3A LALykAC6/JFI4zBDJZZvwkBSVHkciZJblcDwnuB8CT8qMZ1OMGoW4Dag3e1Rrzdodge0u4MQRduA sFpjV9d4XK+xuDzH4vIMo+kYXlOZE1nhtZwlcnrcvLiO2QJNAKu1N2peCnOtI1vM0zX1K6kVgUU7 BQOoY4Afl3j45j2ub2/xzbff4I9/+BO+/+57fPjhA9bLJdpGMnwygBBawY/eSLUKYFjI6Sl7KRRn 6sPYmzsARafdvy4TXiMj5UNh+NtL5/2c3146xg0mzfDYl8yFn2O5OIW6U1Nr8hNfSqGrllvUscVo PML13Q1u7q7x9uGNZFq7PIevPNg7RAREivDk0DQ1RqMRHIvrhCMQoUW4SKp15pspvSkEyslilVso coJm/m8IEoZ5TU5pXEPwNbxWRYVEc9QNtrs9Nqs11o9L7JZb1IejhIuxhI5Kdlgpdw9H8J4QVKhB M80Fdil9bYAItgiJBw8s1hyxWsjiHyLQMiMo8dNiORDUxM8ejoRk23IrzPLtHnF/BG8OqM4XoFGJ siwBT3KtNmhK4q7UtOWEIfTjKWJoUWma9RgCPAMcAg6rDVY/fkA4HEEhoIiAJwURxCCn+Tf4qfaf t7soDK8vmM+BjdfW2i9ZjJmHhsEcdPy8hd0sNz93If9ztPbXts95n891e5z67ZRyc2q+vXSdofac H/OSTEyfbYlgqf5M3gthj8RtGAkoZ2PhU7y5x/37t7i+v8V0PhNZZn77GHWBEnmVWoWQ6nikpkrj iF7Sn/InT58SxUfHjSW5Iy3XnYJn1U3rUhSNAKfAkIRbroSrSozHI0zOF4iHGs1uj+N6h2azRX08 4LDdYb3fYb1Z42x1jvPLM8znc4zHIw0P1Qg5e7ZnrFin+ufUuEnHsvyP2GWZoJ+stKJeRnGNS3Na IUoCuQJUOJSjMSbzGa7u7/C3/7DD44eP+OG7P+GH73/Ap4+POO724OCF3M6iNRFsbKqCpnc0znDP yqqZVuk//Jt//2S2DE2orxUSsxun8188umu4575/iWg41VE+eyYZb9w7/jXQwfm+7qAnXIwcVEgH AiCGLxh1W4MJGE3HuLi5wsPDG7z/5ivc3l1jNp+iKBRQcAC8k4RRqpG66BWlcmL+MwDn5T6tld1N s4oGuUNieoe08A8AxXPWilO+4uGgz8mb+WSJSpykxuG432PzuMby0yN2qzWaw1Gq64IQgiL6SFod Uf62HFLFPyFUms/Rq2skSo4KbkXIgaEEccQAcCAEAmpEBCftE5URDZ0YIQQ4KhAUhERPCJBU4SBC qBya2QjV+QJnF+copmO0FHHUYkxc6rPopBKXC5J/kxiIkVEVBRqtzEgNo93s8On3f8Lm+w/whwZF 26KKDoWZLz2DvLRhKj1tDH2W9khemlc4F+SeWjLyz89xpziX9L1ZPMjqyDg5Tuwaxtn6ecDCtNDT 1/9zt8+xPvwc7f9LjntJ9gEmf+x/2XFysEgcGu7rjk91MPC6rDsFLlxeodMLUAhWjKz0mF+c4e79 Ax6++QqXd9fwkxGo8KDCgTxJJEJQMqAS/HKiZm9sKTqXdUbfiAl8ghTMNByXg28kc8mxKGAEShZO eb+uhpC13Ql2UHfXEKW4YN2CjzWa7RaH5QrNfg+0kgZgMh7j4vIc59eXUohyVIELI6T2x/Bng05G 4jSkd6RMRsNyfGgxOFNGdOoEZsARyqJEjEATWlklvNOiiRrBp30U2xZHVQKXnx7x6cdPePzpE1aP Sywflzjs9+AgRTK9c5oEEHJfkr8O6DJDs3AwiqGgedp9T5Hzk42ysKbTR5zUfJ/bvuTYfJMaHho5 kYEDzhdBAxx2r8E9aTBRh8cQZabrbILLpQnRMWpq4WYFLq+u8O7rd3j/9Ve4ubnCdDZDVRbistBQ UqnlIoOEiDRpjEPTNJJkxpeo21o6jZwujtbS3V+zXsD2UN/tYQAjWRCtXSkDU0QgXcSHICxm2nQC HZEBtlhoRqgj2kODTz98xH6zxWa1RX04AG0UwpDmeJDndAhRaq2AqEsBzMLGFisKi9WGG3CUjH7B 3CH6ngJoSPLpa12cIInSxTISAsDWDmLis0RiFgZGMaIM0okuMupmi0PdAIca44sFqsUM41GJ1gF1 7EJVUwi3doPZ7pwngDxiewQ3AUULHLYHHNdbcH1EAS/MdKenBssrg6TBka4wp1xUv9xS222/NN/h ZwMLAL3B/J/plr/CZ1scONPCs3NkQZGxrUs1bBS4gbwShUp+pxOhqPoLOo1+oCQl1MJWHBiNEL0w v1jg5u093nz1HjdvbjG7PIMbVULM1BrfIbZAZBSgVBk0RMlTQ97pgp+9fypcKARPMMNlKad7bWXt 03uX4XeXPusSrVZK2enRlXu390Z2BkAIGktKpQc8gQovSbXGlYSvr9dotzu0+wN2uz3atsXhcMTZ 5QXmF+eYzCegyidwkcvSRGDVd3Eqd3svkvpGeWcJgWkSMdJyEgECnzILkOA5kXOh0arSJl8zVBWJ AS9AwDuHWTnD/HyGm/sbHN8fsF/vsVmtsFwusVltsNvtsdtssdtucDwcEJoW0BBguS+ndaSgAkVZ oTglqIaWiuGcGGq0FnNLJ4fEies71+9Sa/ATNyOiJ4vhKS3AkKldlDN7LTslCmbINycDCRjJhIHr ux1IUTfpCCWS9N6OSCI5SBdND1TjErdfvcHNmzu8f/8OVzeXGE+mYjKD1gchnUgA2EkZctM0mIEQ GzW/sNxHEyvFICOSnuBtWZyMgOhcmRbPVPckWTc0I6X5z3TcRgjadeTAbYAvxBQagrhjIjQETduN Q0xJaGITsF2t8fjhEdvNBvv1HqGNCG0QgqVml5FYdFmRGYa6IQicAEmkpdEgMUrpdB28GpWqESMW OaJWi9iZQmMA4AEO6Nw8Nh446CQgVf49mBguGqFMWnYSGfVqh2ZXI2y3qC/PUV3MQdORCBzWlO4E cAwCnIBEWWrYeC+SKCzsdtj9+AHtaoMqMApITLoAINbxCbHsMMwAlqwiSSYkVSa3MNhalEtdm0+c 1ODkM31m6wP6p/pcvhkbPf/eXQjZCvllG/U0U9cJzPzyZDKjv8/uZ9ak5wDSZ1ka+Jmnp758s4Wi 13YJHCCRflP7DC2oWd2M9DctEpzAJbLm7Npd+jao9ihSIbeiZoDdFlQnoYe5adsqPXuVEXJslGgI x2AHzM/OcffuDd5+9Q6X97eYLObwVSnkZ1uICUCUiCeRtZReRmpcQGwCGQgXLddeqAPYXYbSrNAX bAHO8uw86bfuuhEMZ/yfHiixZRupbejJtcRywvLwIk8cgdwIviwwP5sj7I7YL5eot1vUxwafllts dw3mj1uc3VxgdjVHWZXwRSGgSrkTQX24RHlbUAIRtkCzzlkePmGmIJMT4GTnxRjBbRA5FDvZTmn8 cAIuvZGc1k5CMXIoyxHmlxe4jveIbYu2bnE8HpXUusfxcEBdN4htixBahNiKZZWk3H3lHUbVCMUQ /X2JSDC3g3Vc1IX3JaWDBhM0R93PnpMa4NQw6M092edc/yjKzHFJOmXPw/3nkjognWbLHK1PQZrl zCvibuIRzIz5fI737x/w9uu3uP7qFvOLOebzOSglZuqqkXLvEfpPz6z3gAqFFFabCY50bIeMnctc Hq4AOxEY6TwDaCztEzWrZJeqRaI0mICiLBCDgANmhvMepXdCMm0alE5+b48Bx/UO608rrD494rA9 CnciKIKWOkFKytSsepplL4I1b4WGjYqJQqNBNKtfzAAsMySBZ0xrZ9Cokqi/s56PKExncLc4W6Iv aX/SZ9B2RE74JJRE8I7QNAHNaotdXeO422J0dYFyMUU1HYOYUIcgBZFckYoNqUNK+DER4LrF7tMS x+Uavm1RsCaa0WOZMtOsCXzu3vtztuHsybkZ+e+ff8Wn2y9Fijx1rW5M21zIMgQ/2UxTz8dGJ0KS JenP2XR+mqY8JNQO3+Gpq8VYC/YOpORc+X7KBTyUm/23V2kweC+Z3lZirFM5TC7HKCHV5LQEuS5a 6qlIeRkkByupfBMXLVUO48UU13e3ePfNV7h59wbj+RRUlYB3mgshU9ZilmeHBpyJ5+Q758CU0vtk V80PfXK9Z7s5zXMdGCcO5Gc+p1voZQzoBC1t7XwBLgtQUWAyKjE6LFCvttgv19jtj6jrBrvjHvPN DBfXF1iciXWHo3HKPJAnOWTWcHtIuzsr4ZDsHhl4ZlXMMvhsSr2ujxGk7pzMPZmhVkr37HLlWLl6 p+tFdFK2gDzBlSVGkwoVppjHS4nCi7FLv67ftevgiOGJ4Ryh4P4ofnZ7DvF3yxc6belEbz63KH7O fZ5wIl5x0/SAgn4WcSUVwDqzM6dj83O880k7kgx1hiY1sTdpcqXS4Xxxhqubazw8PODh4Q3Ors8x WozhSk0HHmVB9c53z/9Kg+fv+ZKm1eNUaOl3IkpEJikTLpnoIhiRFOF6QiBZvEGSz8FDwIDwPBzY QtGC+uhaAEwYsUe7b3DY7rF9XGP9uBL3x+4oVgpNABMg7oaoY8JitQMAAuviKyACaomgqBMnamKp wN3k61YUBCN6RmhqYWnPoJ0WY2ZKtmmYTIec/QYVuDbRzEcb4ZyHIw8XIurNHm1dI9YtwmaG8eUZ itkE49KjJQvXE5IpeQcXCWgD0DSotztsPi1x3O0xJgfHAaCYLSQM04gtzyc/k4cijYk/CyZ8GVD4 JUHF59zjOZ5P/lv+PQefnWXnz0QX1L/EkwyW/HSh5ww0UDopA02mpebvoPcRTb/bLYUkoRo3K3jq NFY716wOPYUtQkz51C2QyV3MnXvY5hN5iYSSkG7hRlTTMa7ub/DwzXvcv32DxfUFinGF6JCyZ+a9 Y5yj3C378zZ5i8/O0PzsZV7u/9eu2Dubs/4nozsxfOFQFBP48Rij0RjFqML2cYVmu8N+vUXc7RE3 ezRXe8wvLzCaz+BGJUCk0W4x8eBssZFqyCLj4guUVupEGmRImbXIa2Vm0nFjhMzBimND02RzGj+U fitI3WtskXEEOKdZpzVk1cYYNP9OWvpFxSoY/cljg+SlRS2PQui/dWbGS7ten+hDN8cTYTNoGRpc P0f/T66nz+W6bzrRTQig9y6S1U0Wly4DmxqnSP2GBeHscoG7N3d4+/AG92/vcX5xgWpUCJp3fQDk vU9WkJcKh9n2nLDtWypc79r5sS7TtgB0CzNEWAXoM5BL5dHl3XXRpohA4vIoiNT1UeOw3aPe7oWk udlhu1qjqYMwCJwIqBCihot2wKBD55xCqXp/La23HduK39aSZ0W2yA9SzgXUJdIBj6j7AXSk0Bzh QzWRbGGwZ0yaKjwigJYDEASUOQAjOMSawY87HNYHtJs9JhcLVGcLlNMRuPJKLpV5UTCBmoDjdoft conDegu0Lbwr0hiT/uIe8zrrsZNj4aWF/i9pWfhrbp9z71xGDAGG7XtBr339GZBrfIN74zMWz2H+ +CfPFGHW1BxYmKIndrdOZnVVcikd11eKKI3h9JwkHCuLunIsRE1OC4gAC3aEWuXBbDHH+eUl7t7e 4e7dG1zdXGM0GUuiKLIoiKfKY1ozPqdtntlMpvXbkbs15RfchmvSk+szklzIdnS2KBKQFZlABcHN xpiWHm4yQr3eICxX4O0O6+Uah0ON7WaH2fkZJudzTM7mcGUBaFi7ueQ9i9vEFKzeIw4er6/Q655k CBHXdizEdQ9Li67KnVRd7gfz2if7fwqBh7nW+gqaKLJOv3MPWIo1itASSSjqsOtem5avAYaXrBzd Szx/zjB5kG3insiAwIlzRdgAiaBkI36o9UgrSC54u6ZO9tKPwEFqToTIKatmURYYTUd4/+07vH33 Bm/fP2BxPhd2tRcClXdO+6Ev8Jg7M/hr7fvcZLLnNEBh/3pbZDgj2ahmZIJB3prgo4ZjMQm6jZyy zQUGnCtQmAuFGe3hiM1yieVPn7BZrtDUNWIT0bYyPWzBD8wIHGC+QgMQiQ9hJjVzYWQ1AYz9bL+R WlAAff7AaCMrOUzcXkKn4IT8bfBL4T/XzbsseoG5G3/mFEpUVY2y8YVHGwNCU4NAqLwUqoshoOFG 6qAcahzWW1SXC4yvLjCaVKIixoiKCdwE7D8tsf7pA+LxKCi+jUmLFJ9Jgnz5UzwZBwKC/zrA4j+N 7V/7fXLTfLaXFIhTB0BkY3ScLuvFbl4agEjnqGXRFKWhcpdkuYEGs07qtaDzWfQutVKYfFNlzMPI jAA5htdrSN4WQouIY1uDC4dqPsXFzSXefv0VHt6/w8X1JcpxCRDQEkBe4A7HmEiaoghQ97yDFvmi 1qZBzh2tF2JcA1vrP/+C+CJQ8nS96ueRyUGT6WkMAjuH1qz1pYdfzDCdjNFORzh+fESz2WF/PKL+ scZ+tcVidQbcNJhcLuAnI0nCRaKUAAxEjeRydg/LxNwHy4kxka97mRiEI3WtyzpBat1lAxncWZJh ZFcCcjeyCSdGN+YYth53ymMCF3oRBsDkQb6QJFqGxuyar3XAS+b6LwUeJ90gr/3+CrgY2jGJkMrD d7tl8Uqd5DrTUBMaFI5QFFLchQpgcb7Au6/f4c27e7z56h6T+RSj6QjwQIytpHUi0bbJEiCgr00J sef1QT+0VuSTzzmX0oGfbBOypVdIkMJhsOdQc2tkOBJtJijhEd4B3ol7BELqbI5HrNcbrD+usPz0 EcfdAbGNaNsW3heAF1JjzZrUyhFQlgKwAJAOZn0Zzf3PYlnQGFIhBQWEtkXTyuLsCkJsoxQZillS JhahbHHe9ug2yA2du+iFwQ6kGSG8DKSwNAF5On2VwMgUVDB5SZEd1VQZg7hJIN/bllFvD9gfDjgc DwhtwOhigWI8QlWWKNqI4+6I/cdHHJcbTNmhdA6uZZTeC9kqjQMFFkPlKR8jrwCL18Dql26nXJh/ rU3mx1Md+Llnei4k9uduZk145ukQKcn+p78mt4wK+4F1NxdNJrRPLckGig1AgMz1YXPcgET3zKDu N+/MrSLEYu8FmMSo5GwG2BPG8ynObi5x+9Vb3L9/wMXtDcbzKUBahEvXNAInBcBlsr/7f18H/pwR M5TZudXZFAZd7zIQfvr8U+3H2TEvudhO/pb9TW9nz5O1edCOjgTAk1RzLT18cYZR6eEmY7TrHeLu gMOxRvjpE467A6bLOWZX55ieL1DNxmCn7+xk/RiGYg/Bp+XCyfem5sjdVmTlGZxUi44RzsBFzDKm 6rUMMDhdb5LOhjwapXPDMSGBTXPiSCSfQxsIxedEfD0HCJ507wsWiycAINtcZtY5fWyHGxPPoHfb /u8mHpKbAOi7RcjMktqRdoAd7yTF9GhU4ebmHm/fvcX9u3vc3t9gfj4Xu6XjhAaZWDtULAGeis6f Bo2tjvysoB7uzZOADYFFbq0YWkf04QVX6sgg1rTT6U6q3adnFo6Jg0OoA9qmwX6/w2a9xerTIzab DepDgxAaQczeg8oCVFQ6kUqMqwKuKuF8qaQkSgLP+sFAACBuC1aQ0YaAtm3RNlKxEE3AdrlBc6wR 93vEY50IRLFlKSYGQRYMJGuHkNazUZFrUyacbF/mGhHpKdYWM4LEVgi8HlZTISKEmJJmgbwwo2NE s9pi27Y4rreYnM1RLRbgwDh8/IR2tUEZo5R4huC3QtsicqbrmUrASIDj5OLFp7VDoz8SDIyeOOhn bi8J4V/6HtmeU0c92cMmQfHzE24980T9bzzYSx2TP9dyKUMOzhZFgioyyFY9VTgUYuT7ZL++k+4W sqbxcrQ/yM5DstQKNwzqggXAESQ0M8l66wmuqFBWBRbXF3j7zXvcffUW86sLlLMRUHhVkiD8LZBk aoRExUk0VniiXHL++UT7PWndAZjINwltPCHXBue/fIOnxz8HQk8pe3kZjBxoyDNB21yS+ckC7pUk z2hDBBUOfjHDeDoFLhvUyzV2j0vst3u02w2Ohz2Oqw3C5QXmF2eo5lP4ibhXg/IVHBOIHXI3FKHr 9w5AZO+CLjkZVBEiPcaAbuJssQMn1zRrcUfJJC2yOabRLeukDi4iSVyYJfJyjpK3IYIA8oAbZaGo wwn0Ut89o32nDjhx/LPXeuaew8F3CtDkwKO7B+vim6Zh18gAjMTJFvOXLYa2r6xKXF5f4Ouvv8bX f/MVrq6uUI5L+MLDlV3620gBuXyQ/4kvykiGTkNNv0S9ZOaeleKlQmL5RBWioiyCtpgmshW6Bb91 EBIPMxx5STbVtDhuttitd/j+hw84HhvsjwchF5JDMR6jHE1QjEcoRhLvTWUhzPHCAYWwoCOzhG5Z 3w76vregwlCxIOmgSWu4jgjHBnVdoz4ccdzvsV6tsfy4xG69QXusASChb4oWjmfp06SfNdazt1Q7 tnh6IC1MMO1AxolLIW0drcrIbBrlBZCDI0bRRMTNAc2+RlxtwNM1xqMxlt//iHa9w5i8xPzHiAIS OSQJZ3IXDSfriykteatZb6d+PzGfOvDWCaNfcvslrRmfBwROAw6TPf05kLkgvsAc/tq9h6DCHMid BUHa+kk+C86VG+nbHl8idl1o4MPiS0xWEFH//jZfqL9odBeCnZ0iAagQ0725d0fjERaXZ3j/7de4 f/eA85tLjOZSdiB6JP+6rBmMGEW+OaJUztzunRZZbZucZvxa757i6+VW2s6il8mLL9y+ZBycPDaT 6dTfndyrEpkn7x2UvO4cgbwAsTZEuLJAOS4xn01wfFyheVzjsDsg1mu0uwN2j0vMri+xuLtGdT4D eQMAZOKpayP764DkX8/6gNJuBxdzgrP+mJRu+96lXDAZHA1kBHNKaWQQOqUoRJayCgpOvPMoi0ID ChyAApG9WC6cJlsJISQzh93QGt86IMbY08pPdtag4051JoAnIaAAMrn5FGnmi5WFwZwiAqU4gSgv 7r1H5KA+QwMTuvI6IBDDlQ7T6QSL8xm+/duvcXFzhbvbWyzOZoLadfI2sU1gJAV/ccwEQbaygxSH nn7/eGLwE4CqqnoWi2EbBq05cqq+BwB4nf0dqJAIdqmLYUxyEV6xZdS7PXarNdYfPmG92WJ9aEC+ QDmeYjQZoxiP4EclXFkBhYerCkTnJRU2CSdC0K0IHQt1S+t33jc2WV33vorx4HW1pRkBkTGxgX6s cXmQUumxrrF6XOGw2WK9XGO72kgJ5TaAQyt5LpIW6CSyJyq3RKWF9RGREDFZU4+TClGCz8yfxr6W 8R8cwBzAShr1ELcJGoAONY6bIw4A6t0OFYvVyIUWHpJIzBGST5l0rJK6c0wXTiGFaqnoqlBaG9pg ewpaCSc0sVMWri/YvvS8tCB8Dog4ce0nIZcGjJHN7dPYAxa10YHY/rNIdIUdi1zM9BYy4v7f9GCE zhXVs+pQl/rULHVKgRT5ySnEXPGGuknkWkkWEBJPylwhxgfzcBnBTo41hdIelFWWE8ncDI5Bhcdk usD9wxu8//Yb3D+8wWg21urMOn8NBOkKJeNQ5wmQss+mZqde00mvUPeFMkDVNZV+V46BhPX31xvm TGLaOTQYg7ZCxo7DZu2Z90sOGoZK6nBM98BzMjHpO0WV69mYcNZOMd9HPesVEwlnpSrhzj1GVYli OkG72qBdbbE51tiv1ti1LXbHI+bbC0zO5xhNRvCFuht0ASfnQA5iLdHvuXXYEgum7NR5E+rzMex4 5fpoOxAhgRpmQG+hKQE6oFmHoPWaYqcYEcGB0cYo1lkv0Ycs2Yc52VW92NR61ddyRHlqIT+1vQQo 0nf0zf/9Y5+eNwQhlqL2lImLOaIoCs3+FtFyC0aQ3BWFLvgOwnj1DtPZBDd3t/j6669x93CD8+s5 qrHUlmCWtNTGyRDrQBL/2kiuz/i3+OrB8+VCPh/aKdFVFgGS3D8nzh+6RZ7eJ6ZBx0y9eiGOvJBV 6waH/QG79RbrT4/YrDfgNsAVFRbXV6CyhB9VAiyqCii9hrASGjJ9H8lK0hcIWdNgsNGJfd1LAKQh pU5cMK708FWJcjrB/FyE1u17Rrs/Yr/ZYbPeYL1aSdraT5+wW29R72odX041Qso0QgnZlfLusRsz QfJ5FM5Lgq5s8kVy3WTS+gQ9C4McKBOYW0nP3jQoGXBqsSB0hLyIgGTwzLXwDJRmnfmkj7t+HwjI 59oVeCJM/5Ib47QMOLl9joY5BKiv/s7Z5/4hw2iHk5qxLUg4LZ/0sXv3EKAi41dyngjSZtawwF4C P6hsIoDYbGdyP8uqqUnYDEEIWNA5p/e3/ARi6URqy5YbwHsUVYXFxRnu3z3gzbu3OL+9wnQxha9K RI2CkzlhpnJkmjqlFrBIbeq12+D9qd/WHaG+L5/zz6fc7dZK6d8Jax2pxSYaoIL0I8d+aoF0zYGl 69Utf6wOBSLXmFJ3WhfZvtQ4CpzUusVa9M0XBdx4hDibollvUe92ODZH7D58xHZ/wNnmDIuLBaaL KcqyQuFI8vmEFnAORSHZOaNx0UhAjHGJrd0NAJ966zS6NerDLN4JLBQOcIUCKkbTNGgC4xBaNJEl SkbBFSDcvhAYniN8DCgR4RFROC/C1vwssW1ToZmelUCTObkTwuApWfJEf+U/cwc+h791x3c7+5YS Rb2knWeCXREZFOERSdZMkAING2ROqo2ORhUuri5w9/YNHt6/k5K08zn8iEAlS50HxGTNcRoFkvVO ptX0AVfk0CvXbW3Y+zwAE0MrxXBiTQMfdAAAIABJREFU9iwT2l+nULlc26PVAUgMFKShsE1AONY4 bLdYP67xuFxidzzi2DYIBIzmE4zOzjGZL4S7oW6P1nsEEqHWKmk1PSO0/bP2iDl0/jM3BoQs5Tyo 1IsGRjUqMTqf44JvUdc1tpstlqsl9ps9Pvz+T9itNliv1jgej5rJTnLpN20LDy/l4OE1X74hIUYT WrBzSZDL7pgkurGlk2ZrXJqOXQoXGD5GscSQQVEVgarV2dsJyMi4ITqe5b7PCMRniFK5O+xfc/sl +Q+vXetFYt9gzn0+f6QT1qeO7yyXJsCQBDuI0YLNd9Ytopm5xEzpPd4PAEDmf6E5Vrh38S4tl3de VnwOYlEgoGDhBwUHFKMRzm4vcffuLa7v73Bxc43ZxULqXnitE8KdVcXpuHNAGt/JM5hpDbkExonP tpolyyieBxen2rV/IaDzvWSQkERZEMNzJmTZtOnuKU+NjZeCEfL3SbxKU/GzF8zbwdrHZecZKJI2 7dqBnEccATwqQPMxwmIEt52gXe9w3O7RrDc4bPfYLFeYXyywOD/D2WKBclSp9YCBFprR16UwfGcE H5hzLWvrHrqm7C8BcMr9IlGYPUDkAXKI7NCEiEPdYnuscWhq1DEod8NlCTM7EB2DybMARgT9n//2 f2FjAhtpJzUlZVkqM+132DlP6pNkfX7SYoGnA+65DjdA033PW0x/zycxgMCtWjYcyDGcd2hjC4BR VAWmiznef/0e3/7tN7i9v8V4NkVZlXCeECkiUA34joToFIWKP0onN9Oz4IIpdnD/9EulpFd5nop0 tWcmhf09ZT2y3yNL1AbrpPQsKai5DtguV9g8rrFdrrDe7LCra7jxCNXZDMV8Bj8bw49GyeTGzsyw TguG6WAmnwCipAAXEGPjIDjgObHxORslre+0Zu6ogDGYDXhKuvAWsWnBmwNWH1b443ff4U9/+iOW n5Zo61auwQRutbAOFYghIIaIwnsUvkLNAY26PwAa2qiQT2Bt8I7Yq+YNjpxIqNo7WlNFJz5bn8XU d8maymyZep7dJP7nmd+Yny1Mlh/zJdtfw+rxS0d9DK/52nV7iyC0l3Nwktk4Ov5Sp23D5BQROAa1 inXk6+RahoGQTuO3m1q1Tkn2B5gFI/3VMgaiuEiBu4LELA2OKIsCo+kYV+/vcf/tV7h79xbTxVyI mg6SzrvTCADY/JUoEM/5OE8KOLKT0v5IfWtFAiNASgaVX+s5a/YpBUqzPyRQoT+eXiuShZaT0iau lX5xxVP3fm4T3lquQFBn0Ul/n9q88vw5Bjx8dhBrHwQSOwExw4UI7I4Ij1s0yw14X4O5hSscZtMZ rq4vcXl5icl0oqBU35Ggi7ggQ6ZudDobM8MHNCVKrWJBsyOTlwreRA6BgSYEHOoWu2ODfX1E3QZE olQVWm5JqZ+dyhzHDA9CwQVcLFBYKlciElNZa+W6YyK2GfIedkdngegPvOeWltzCMRwovc7WQUK9 3+26CnisGhx1IMhEgPcOUh6EpNImR/hRgZvbK7x5eIM3b9/g8uYSF1eXqKpC44LVUsGtlN/V2xmb VpKdSLEpG2F9DJu1wAlgkbs5fEbSzIFCN7lOgLJnvud5IsRUCLQMFL6Ej0DY7bFdbbBbbrBZrrDb 7lEzA2WBydkCxXwCP5/CTUaI3iM4C9dUDwCpZk2AMwKQAolc2zF+bOrnP2NLhC7u5oj9A6C4WPYE VqFcOLiyQjkeIfoCd/MZLt5c4e3je3z44Sf8+P2P+PD9T1iv1iAnri3vRdDHwFpATVxnHM1SoQBm YBCm4bNy7J6QACBKXQNYDRRO7aiqAkwTZG3j4Qw62S6vttl/vtvnWxW+/JpffAxnUIIAIgHZHZ/K hHRXhEuAglYfdg6erX6EZX1VIOK6SsBsctU5OOfB3ovF0HsJ63ZehL4v4LxWHXUOo1EJAqM9HnDc 78B1jdlkjPs393h4/4CrtzeYns/hR6Xk3/HiAiZIHhoGkjVNYIMqJmTAKmsKIFu49Azqz0fA6pJ0 538uPMz7u0vR7zqrdtdR3d0MnKfnp2QpgAL1U0pwd6nPGBfc/U0gq3+R3g89vo4qIJQ1RF5sERDS uIMQ9v2sRCxGKGczxEONsD+g2e2w3O2xbxusDwecn59hPp9hMploAisFT3bNaCuvEPqdcTIGapFE E0ouKCanSSE9mAh102JXH7GvGxzrgCZEtBw1h5OXNUFD9D0Jfd4zw0fARZbPDOGggUD/27/5d8wE lFUlk6UNKJ0UZ0LICsdkCDHxLwZywIhURskZmsQ6vlPX8ScRbbb/Kbjo3ALWoMmdQAAownlCExsQ OczO5ri6vcbN3TUe3r/F3ds7lFUl0R++QEh8DInyCFHQoCPLqcDpeYwLYRkgU8eZDc2o4IM2Sefq cxqX41nNip+236njhkROi2muQ0RzbHBYb7D++IjN4wr1oZFzfIFyNkO5mKKcTRFLj+CdMMYhqWkt LDe5XXigUWUauMwfS9HVTcafq3sykJKNAR2ZLW8pUpOxTejcagNmVM6JuyJqiFUTsF6u8cN33+PH 73/Epx8+4PHjJzT7BuOygncFYhP0tSiFnJrpO43zhCWfarTMqo1EBjehy3bK/US+zOI2AZAKyMXh mIl9bc62biHrWy5Ouilf2P7Slou/BFD4udupZ3l5cekvm6K0d5ktDBBwWlS6MStaI6XF1apRgoQ/ RL5QEqUHO+F7ucLD+UIUjqKQ78pxgvOA96DCwxVFAhditQC4qRGOe0yrEu/u7vDNuwdcXpzBVx7O y7gMHNNciRzhkuKk2gEyDd3aJ9e29Qed8QNobedZInJr38/PvXLSbWJK7fA5crmo1s2hLBStvrPQ vChnn9kYsTfHoYtx980OzKCZPVt6WtWMtO1iUhpNZjKgbmunwJWZgRBAdUDcH1Hv92iaIyJHFF4C Di7OBGRUVYHCa80YmJtLFUOtJ5M4O7AbE8xqISCRUpjysWlwOB6xr2vUISCwUwu2ktm1jUlz/5RM 8AwUEfCR4dsIHwEfZTywq0D/w9//N/y4XmF+dobzi3MspjP4llEVBRxkIY0xpkqmuVk+Bws2GITp /Dq4sL+n3CbPWzU6U5f4ezqhQY7gPelDRZTjChdXl/jqm6/w1bff4PLmEsWogivVX+pVsMcWIHGd SD8xwD4BFiMKpUIvRMizRnSggtOkyDki5v4wklfP1ZQNfCICLF74BaGczHwDs5xZMFAzwvqA5eMS Hx4/YXvYI4DgRhWq2RzFdIpyOgbKEvCCWKMDCOZqYEQXdcCq35AdiF3fkgUkQhhTxvBmfLZgeW6z zKL5tfJ35WiEVaNIdpwbdow2Nlq6XmZ2wR6IQL074Lg74sc//ojf/fo3+OPv/4jmcJRaIJoX18Gl svCmjfbuT/2nCRDrhvUJRQbXLbxOSCsTb4CLucug2hUo4wRmZL/rCcRunNi+/vG/NLh4TRi/dv7Q 1P3l258zgmw+9RcH26/rz5N75CTZHqC1/icjBpv2roI6y/BLlu/AO8TCSei693BlAVeUkgumLOBG I6AQPhMVXoCHcwIunIXzqVB3TlwaKh9AhBhbEAdQaOFjxNViiq/fvsXDzQ0mVQHiFp4keJCZNfeB zBuT3TZjO6tEPwInWQey9V5cFZRaVP6Rht5mJRYGSaCGWy7D8/YX9zsjtlLksNvXj7nLLb69mkq6 PyAi5PG+6I/DV8EFa+BlUiq6vk+y2do0H0dsmlVnrTTzBWd9IYn5uJMZBlgVDPkoFgCoq/d43KM+ 7IDIGFclFosZFrMpxlWFsvQyLJKCZ+iFUn+Z9JE+1OzGEAtaEwIOTY1D3aAJLQJLEixxgUidKMuk 7EAonUfB8vxVBIoAFG1UcKEuWXJgX4H+i9u/4e8//oCiGGE0GuFicYb780uczReYjMapg1OEgqaA TYOLkRZ6AhQ1dqFjOUBI4GKwX/6a+KW0UNjROo9TxzOz8BXUxMgs1f/KSmKKb++uJZPm2wdcXF1g Mp9IiWxNlsWaKMa8Y6SrmO4CcZGeg9Mgy/2VKfvBAFfIMxfKtPWW9Mp3CUgAWZBIB2haPHWhcQBc L/H7CU2BKOWLJ12IDvs9tpstjus9Dj+tcDgccaAINxnBzcTt4WdTFOMJglONJkrtFOg1bHK23IC1 uBYxqQnPZ64PjXlWDSEquMhJYH8WwLDxY6a+TEOxxjKeDdjmO4t4o4jWq/UlCqnXsUPly5TPgxvG 8sMj/vTd9/jj73+P77/7Ecf9XixMDPgWmsDGwDJ675cEMyMJ8Kgpy8EANUGKvgFoY4tU6YQZHKKE CgOSBXSwcaYD5jwb+Rx7bRHp9OL9lwYXuahP8tf+sh3Pwm9R1xK+wJ4lAPrkL93zmCA/eRiLsM3m HKd/PPjeXVesSABcZ3GQXAaSzwBOhK7zpWRs9WJtKEqxPPhCqmXGqkCsxKXhNGRbMqgVYoJWt4cA e0r5U1gXHh9J07S4NP6M42Dg1rFyLjhi5ICzyQS3V1e4vbrAxaTC2AsXCiGKCZtIEs9pheekjdrf BDa6e5xq/5zMnciK3LddsLkAMwWhs0p0fUwQUB9CQF3XWsq7RnOUFPsxhKTcmlWCgGQBds6hLEvJ iluWkmuhLOCrAq4U0CZ4rP+OnSWm23rghTt+VHrvdI1he/QvwrAkdpoHYvDuAi507iflAhpCnB5P wY25eiI4BIkY0WerCofZZIxxNZIMrDQIfKCOGcYqvMSrIGUaGg0rbUJAGyRHR9ApFRMQkc2h49EU DPgA+MAoDVyEiCIouADA5NG6AvRv/+G/5R8+/Ij1eoN9fYDzwNX8DH/77ltcX16lnAplWUi5a0X2 HBmF98nv7ll81c470dRU8NuklDVZmfNquikU9Utly6gREwVilOgAI8R5DelJQMABReEQEVC3DYrK 4+L6Anf397i+vcJX377HdD7BZDKB8x45CSYhIgxGRW+kZbicWBZfijZ6MhmpokkICeJrJYfCV5LL H6QhZ7pIocsJkcoVk05SlnZxWo67GxQd0YtZLBSehHjLbUC9O2KzWmP1uBTGcRtxYIBGI1TTMUaz Kdy4Qiy8JLtyHq0tSnx60oc8Pp8NOArIc2pSYJ1IyS2gaFmuIcQemSgyaXpg8pXFzUyIyBZX+8q2 XmSXYA1GF2pbRDQrtroXHKiLQTdA1ka0xwbLxyX+8C//gt//5nf48NNPaPc1RqEQawcs1jta3jzJ 7cFZv8eoYpVU02gU+Bh/Jmq11q5+ig7hzP1BJ0ZjHDTTU/Nu1m2DnV8GHoabc16SmnHs+i4bL2ZZ YjL8pwoIC7DxkDFArG62RL3XkEpl9psFIYEVMk24exbSfWBhpCe3bKY7p+fW84RQDV1ENPEZGMGE Jok1AOaacLLok3OA94iFB9Si4MsCRVmJ26KU43xZiqWT5Hwy7dk5CeVX0JAXKGSNEIEzYN6N584i ki3ajJ7VrLc4ars4UgARBbBORmPMJ2PcX8xwd77AYjLCiAglQ/KsRC0IwCyKFnltG4DIwUHDHCn0 7gUYCb9rdai8sp5gHRvOebEecgAg7mXLwGx969TOEZqA404S5K2XK+z3e7RtK5aLxL+wm6UBkkCM 4D+XrBeFWorK8QjlZIRqVGEyHWM8HsOXRQIVPaCWLaKsy69j6ilSgALPgW+kz1jJlCtYGGgHgE2e 9UB49nZP8rbA5pstWZ2lQ5KZAYXX1O5mtbF50XnxrJvkNFYOGARgpFLvyNuAOmsWy3MWEHeHZ6Bg hg9AGdTCEiDRcWneEwIcGnIoHt68xd3tLVbrNT5+/IDVaoWCnbDnlXgo6LFbSJz3IC+hq6KpuJQg JrSNJFtKrWJEKAKcIDHvFUUHkUZEBOc7/7qZkqBCRdLakhI1Her6iCbUoIIwv5zj/uEWX//N17h/ +xaz8xmqSQlfOAEWMXY5+p9XdfrdSqmZ5QwDFjaotEdSlIobpOimLsGJIVe225ByIzimwWcmOAcB YxbmI3Ww1DIDB88OHh5oIvb7AzbrNdbLDXabDQ4HKbLFZYny/AyYjFGMRnCjSgmqMpqjtmlHTus3 AUPRslmqdHGJOjFiIlX0fbU2wI3NnnowQ9LQ7+6VxS0MNPYOClotELU52XWSEBfxTBoD3sV6dHwM OayBLxzKYoTL8TWmZ1Oc31zgn3/1K/zhN/+Cw8ctKq4kBhydZgZIO3gyF6FO2hB0URCrVYzCVk+m UGSCIRc4ZAtpttIQo1tdX24na/vh9vpZL29sYEr7PifrwcABVLgSAG2fQoEy68LCCkg5A6eUmyFV BENBCCXgmr+BtrvKBubsWfRtTYDmY1jkjfqNvXEcPNjLd2dp7MsSZVXBVyV8WYKKAjQagYpC+A4K HgyIwCnApG5uMAFtsu5QGqsn+yQT+tbt3Vt2B/Ui7qRTusXIFnUijRkhBGa0dYtju8Vut8V2u8PD zTUu5zNMvbkPFSSYBdfur+4IUSjkyJ4VktC5RO3hTQaqgmjWO+GsAZYcDNCcDCzRWZ4kWqs+HLBZ rrF+XGK73qI+HNG2LThkLkDKW4TSvOjNDg4IYLTk0HiRv/vNTuoklR6j8QjT2RTT2Qzj2RSjyQhV KaAqIEj6BbuX1/GZUy44G48GbKnjoyW53tsomzFPZ6NZYdL3J0d0c6ubd9aDLNVsCWgAtGr9zBw3 veCe3FJnXPJuTaLsEak3/mxzYLhoxE0DFAwK5jJX8Usafc0aiCrGA0JZlLg4P8fZfIHQNAjHBvPZ TFwggFXnRmgjPn36BFd4KTFeiPm/aVrEGDAdj+CDV/kpcd4hipj1vujnijDCpJMoDCaprokQUHkn acLA8KVHWXqEGNC2NcCMYlzi/PICN29ucP9wj7uHO1xdX6KajMAOCFEjPvThyQbrZ25DYuXw3Ny/ Z3/zf61WUu0U0w7hOwA+BBQqcFMYYnIlaSibAQ4icbMERjw0aA61JL5ab7Fcr7A71mg9wU1GKKdj FOMJqvkcMIa503vYwDqRIyGfG6l70gcDIx2oIpXkiaxqIXTabs6CSobuDN3cMAnIk/bvwAVYCwTp dyNBEksZ9sQSRzdfSWflc5wE0kWCQwRTxHQxw9fzv8FsPsPt9S2+/+2f8ON3P2D1uERZlhiVFULb oG0ivPMCXtCBJyapmMpEGgKd8TAy/cT6IYHNJ+3OmfT+19vMYuFI+1rlADGAKJqMYCEDRXKcVyMF ExBTNsrOxWD/OvqS1DcAK2+GXS7rAP0cWCiskQjwhEhOIht08e9ZHgThSfRQUcCXJXxZwZeFZpgV szk5ARooinQdIuNFFXCi0ahlQZn+hG6Oklnskgoy2E7BvhNH0dPx+dqp3QKnB2s3hMg4xIi2adG2 DbbbPW7OF3hzdYmrxQxVMULkgBRvZRmODeTGAERo3oTsfky9R1KjdNKGTRc0AiDUaiV9KlYKRw4I jP3xiN1qK8BiucJus0VsQ3KxxJC7/rrneH1GRMSgXAzNwFwfgONOEgWWoyUmswkmsykmsymqyQhl VUpSKhLrJIeMu5VeOAOvZoXWeWr9nol5Xbi7bkxiNP+cWaRe23qAszcuTCZ2z5HEB3F2X8ouklme +k/e29NZiOU6UdMQEEQ5JYIYBSIQSOaAY7PyA+wcfFmi4CAuCSJCVZbw1QgYiSskxihJhrwHO8J6 vcI//vqfQJ5wsbjE+fk5Ls4vMB1P4HyBFhEcA7wKJV9U8M4hxEz7VCRoZsRkD3Fe0VcEUURR2u8R gQPgGLPzKc4uF7i9v8X9gxQUW5wv4EqHSFFqfUAsHC9FWvwckllu1jcCkYGLHGyIABJhFJ1ZPqzW mZo7Q4SHpEkVq4BNd9Xy2CWh7SARPMftDuvHNbarNXabHZoguRj8dIpyMUExmwDjCuQLEHkQqVlf 2zs+mQrPvKe8ZLeDh5qkaaEZqMqAhpzOJ4FFD+1/TpvD3EoD8iozKIqmG2ylyq6fEtrYdQbPYdE+ ngjOS/ZN5xyu725xdXmN+7u3+O53f8Bvf/VrfPjxR+zrHTwJO5sTt6LTTUR7KxA4oolBCvZEglRx tbJiSgIBepPdQKSEOj4FsUMi2rBd/5yxfGqzHiKdwyz5f3v9yyponIFMe0O1SrROUuqb5Y6zsWcZ K/VBwJr91BIXm4aVYu+dRDJR4VGUpbgr7K8CBV+U8FUJpyRJV7oEIMl1lgfyhbgtwL2oD3FT2tyD 5obp+sncKgkcoSM/ngYWf91tOB4iFTiEgHq9wXZ/wGa3x835GS4Xc5wtpiiLUqxLLJliQUCh2A+a 2jnBf+r08G4t0naLZn1lBWUqF/REc39wZDRNje16i8ePn7D6uMRxd0Bo2uRWFFMKp7mZj7V86xHg 8XQsi0wGzHIeAiO0AYfDEbvNDr58RDUZYTqfYr6YY7aYYzQZo/RaCiCyjM80dvXFnY7jyIP7ck8f 6KZ2f+7+YpFTBujT3fs/2bMZSMjbh/JzeLg3A0AGHnPskf/zBDiHELoIuYIIvvBaZ6SEH09RlF58 UZGtSmULR4RSLQ3eeTShBRgofAFHDuvNFpv9Dj98+hGTyQQPd2/w8OYtRq5EWUpJ6RADmFs4FEJI YvHDexXQeSMREQpzrfgIxy3gGjFlO8J0NsXVzRXeff0e9+/e4PzyHOPZGKPxGNFFtLFB4K7eRko7 oILblqdEORr089DfNRTohu6hf71m/LR9+XlJOEN8VCajxHdlPjGZnME6VM2SZpIsqBRQ1TTYbbbY fFpjvVphtz+giRHsHdx8gmo+RTGbwY0l8qNFVLNxF21gdQOM1a4Pipc287F3lgprB/ndLBVWuMeD ev15qmZKuu6J9h9u+QLK/NRUm3yMzHAKxBisLqRMJFh4Fqg3ybz3mj5XgDNCRBNaEDlU0xGuH+4x v7zA5d01fv1Pv8Iff/d77Nc7AYXOa6E0h8haLVX7VEyqDA0y1ZcxgCGa+vDl7ZuZqPuaxQttmH3/ JUM+WdAvWLVVIvWbK0GV1SQktVZcmk8GXhmiQRsxsiOHcYpMkhBLB1eUGi3hJIrCy36UJUjdFL4s Uhp4KuV4di6FcgrnwawWusCpT5rglD3EvXBgs0jEjnWui6hyu1hAuTV1UPAnx5rWPrQ20Iv99tL2 pX04BBM58AOcuBVdCfgS2xBw+LjEh+Ual2cz3N9e4/ryHLNxhaLwiKEVoiCAiowrJC+aGdrTewtA VPmdcvRQstQJ0ZTgyYOYUR8brJdrLJdLbFZrbFYbtMdGFC39jyMj6DPI3RgWwJ2Tmk+1QW/xVIW4 swxwR8YlQnQBoSHUR7H+Lj88YnY2x8XlJc7OFxiNxyDv0VJEi5iCBQBoCLAou2ndyp8H6HGFhuHn X7q9NJby9SpxGwm6wqVfuv/n91czE6fP6J0xVCMBdBmXjYjCAGsQgIMGe5CDL4Rc64sKbjRBEds2 IfLSeZDziYyXqqTFCEceV1dX+K//y/8K6/0WP3z4CY+rRzyul1jM5prh0gtwcA6OS0kcQlH8mAqF hIADJeyI1vf/k/dmvbJk2X3fb+0dETme4c51bw09kE1204QssVskREmwHiwR0FewLUPwFzBkv9gy 4K/gR/nd8KNtyDRACH4wPJA0JzVJyVJ3V1VXVU+s4d57zsk5I/beflh77YjMk+fW7Wa3YbADdeqe kxkZGbGHtf5r+i/nMp11CMTUgQs4SYwnY+4/esTbX3ibN958g8sH95mez/CVp0uBfdqpa887KvEa K45BE5MGjKJl5F5z99sCPdXq/GBRnwIW+vhF6FoRTK80VNF3UAioRASX8zhThHa7Zb/ds1wsuL66 ZrFc0YaAG9X42Rn1fIJMJlTjhlT5nDBIKQkbWozQC8a7whS3juIBsGftQdZxGMRQsg1xMg15dBx8 7+uAm4P7yeM2mEPJN+YPrOncQtjasJf76YX30PMUQiikca7ShLY2BtzY01Qj3mje5OLynMePH/LB t9/j+Y8+Iew7UhdUeDqnrsB8b9bW2OC/4q0BUDVvx9H92CnH5bccnfuq8fppAQyXQxCIeoYks0Jq SS0EEWKumsDc5SkpoM+5RuJrDTP4WoFBJoDCO5IXlQe1RyzXoampmoaqUY9E8FrO6SpfhGGUpKA5 K4pSVpeF6vE4OzgA/0noE95sLZGXYvlsJo/La1iyh2K4lwaD/lMZ7/5y/Tp+3eMuxZvE0SFWXa3k iDHQ3qxYti2LzZaH98+5PJsxqT0uaQKlGoAmI6QHUpj3abD57PGNaygDUJcECZFuu2ez3nBzfcPL 5y9Y3CwIbW4eWcIs2VMQUrmOhgxjmYPXARfHxqB3WiViHT4tVCIuJ9rGRGwD3U7DzPvVlt1yzeW9 S6ZnZ7ix11J2l4jxkKNCyzNjGZhjgAG9qjk1kz/JPJ860sHvFm7tPQ9HX3r4J/RzeeKaSaxMXs+W wTUTlhOn9A2u8tTiqbPHoqpqnG/AV1QeUVQmghiVsfQC2jv1YsTs/rq8vOT83jkPHtxnu91ydX3F fDpT4q2gnw+SaEYj9tst7X7PeKztWEPXQUoKYkAZyrxmFlk5qW8aJrOa+4/v8eSNJzx64wkPHj1k PJ8i3hFdIhIIdBrOSX1lAvSJssPBfy2FOjisF8kpgAGDBX/CENdM71TKd6woRKtFYvYkSKkecQgu QNx3hO2esO1YXi1YLzfcbFZsY0caNfipggo3GiNNDVVF5xSpp6DehMqp0uzoicyOjyGyL+Nz4lSV H30m8kH458BSOvrckduuv16PDD53Psoq78e5T6rKAsUAW1EeR4lWaRCaOZqv/X6fs8urHkh6JY4L bUeXOpxzVKOKs/vnfHnyC9y7vOTD997nRx/9gM3NknbXEmOLdzVeUA9Gzk3QRnZZialEGtj1uo80 LyRb+IPktzul0tEY/zS9FceHz7kQkol0YoJ9SoTKI+OGzkluxKTkTt7p/na5zNI1uXuu19JAI4eS ymufmBy+SNmDYWV4QawaR/cGfn+qAAAgAElEQVSIJXEqiVvsTaiSAyJFog5d9zHztyhY6z1aMR7b ZlIUqYFkDVMGIlrubMlxBgrhaL8M1yUZhPwlgMfryKpXzX3CwJEBhOzVSY4QA2Hb0X72gsVqxaN7 Zzy6POdi2lA5R0pB24YfKx7pgW++yXwfaI6GoCG0GGnbjt1qy+pqweLqhuVyyWazIbRB95RVIoVO y/4HMpJkeVTmb9InSgN5YErerPRbMypg+X4IhZ9J950NkLJOeudJIbG+WdFu92xXW84u10wuZjTz sYbccvJ9jFph08PQ4fAcypfXtmJfcfy4e9y8g3pvUqrqDMbZHRX+nhOXHibxDhmXwUpocx6N09+8 q6h9TZPBhZdB6NELFUAKsVji2tei7zOSUqKqvMaTu6BuShGmozGzyYTL+RlVJtaIMeqmdhX7fcdH 3/8hH3/6CZeX93j65Aln8yl1M+rdKQKIombvHReX59x/co83v/iUR2884uLePepxTfKipVwiKnxy 2/PKiK5S0rbZWcgo+u2RcBm815gsEWXQPFao9vnyt/QegQIbbRLTYNMMJnWwO4vyJsB+t2dzvWDx 8orNYku36WhjJNYV44s5/nwG0xGp8UTnSzvmlIWts8ZcaD5BzH1RbrtM73jm4xdS7wo/zivRZ8kK 8hik2Did4m8YyNzPm4VjsCNytEGyZ8YqRobeExFVXnHowcjr2BJs3XDdJAXEFsop+QECySmh2Gg+ 5a0vT7m4d8Fbb7/F+99+j49/+Be8fPGSmFpqVwEJSVHDLLHHCKYEU97Uzrww+YSepfN1RuZwTH42 AENrQyyZVis5ErHy+LMZo3sXVPM5NBVUGspwXhMqC6tkVWWN43Oll8txbP03pux5yAlyWl8/KH3O MsgiXFLCX2SjpMpAQN93OaaUUs8JEVwPZjGPhQywm7nB8ilmPEnmA0DstUPrHTgoIyz9Ymy/y+1k 3Z/2MfR63dqD+oaONboHAtJ7eVIk7QK7zTWr5YL9Zo08fsj92RjneqNnqLhvPU/e355MEJiga1tW N0turq5ZXS3Z3KzYbbZYlZTEBDGogaXuAKCvMimlp0VJ9kbC7a+/LdvL39nzYYAxJuvxo0DfVTlx N0DyPYV2DIHQBdarFZPFhLMHF8wvzxmNxzmP04DNYH7zIKm8SCWsifSegb/MHn3tPS59yNqAMAzA gt6I2TVqNKbDc/UDeq5SeB/KW4Nzgurdxlc0rqJ2NRVG/qg6SXW1o+qRjRSkboV0huQtS792ymuh BImCS47K17iUkBCpcGjttGOx3XJ9fc0nLz7m5eIlH3/2Iy6mZzx++ICnTx5zcXbGdr1iMhozO5/w 9I0nvPnWWzx8+pDLx/cYzcaKoivPPuyzMjFXrW7wGNSNWecYupeKNnT4Sg5CAXYUz8PtuSmK1HuP P2rgdvy7Vb0Ua3zgogO0pTnawMvY9qwOyMdeGIa2Y7XacH31kquX12y3W0KCajSjGjWM51P8dKKg onLZcMtx+yS5dE8534Mt5hKTKevlFkx1DBbtQDiSN5qrtMOi/RiYGlzxxO89eDjl+nuVG/P49xQO IuSFZ2MYaxT6sI3dhp2jSkqVlQGMELSDZEpJKwVOAK/ymktgBDg+cyQ4YXrvnPF8SnM2Y3Jxhrz3 XVbXN4R9p8vAJWIIOFGwoZu5j4TK0d8GMktGwBHKu0uwHCe1HR9/GeBhFS3e14USPVU1u9hxcXHJ vS+8Q3NxRuuFLmnPjCjqek9e4/SFJyCX7CKSlYpgqksSSLDHtlyUDCqyhC6ewbx9isLrS5nKPjTs nqQnPRssDHu6ArrNChjG5/ULjL3gaExPXK7wvwxeV/IrDtbqT3rcNYdDD+qp/DCfMojIr5HvSZIr JEqCY7fv2G47UnJEcksFp8+kieCWUD34XsvFSQqaXYR2u+fm6ornn3yqTQK3LXQZRGSkLrG/35IE mj3lxKjhkTyISrx2d+PHu9Z/+Vs0B8DmcbDDSV1AXMSJU+I7M8xSpI0tXddpC/TNhvXNiov7l8zO c0fZ1Bs1kZ7YC6wCKhVOovx1r/bwvuK4S17mdznK6jgQxQZwRBw5rlMAhc5dUeL9BwYGnI9CFXqg Kk4gs3SKOMZNpeRdmR5Bm5c5JY2UbAA7r6WoBzcFJWnAaptdHkCrZ/W5lLBwXOQbTiFqDLuuuZjO +fLbX6Bpaq6vr7lavGSxuOJm/ZJm5Li4N+fxm4956623ePT4AU+ePOL+wweaeDiqaIl0KeDRGC+i SsE7p7wCkvBSqYLvErGNVDXUvibEOFxRh5OUBY+FPPRxe2Ah+bXjuPjw6OnQM1KOsXejDkjDIrmy ISUqcdp1MEC72rBerrlZLFiu1mz2OzoSMh5Rj0bU83OkqZFRA74XCJbDPVROeWmZkamvvWJjmgel PJGocBzmk1SVO1TeZfGa8r2dCW1ll3eNWUkwjZG6rg9eG77XP1U/b3c+y6kX08DCzHNs80WmEU5Z CZV7pV8TOkY6ouYSjyYQvbr9Lp88ZH5+zhtvPuW9f/Ntvvf+B7TrHZVXN6tEKXFZSdoYLWYyOp+B TR76IqCs8mLo3jz17J8HLIbn3Dlunws+NDdKBb2GL6IXqvMzRvcvkbOZGschELuOLia6FEsn2Sa5 Qg0TUSXSw4GBb3lg2JS1LIIkDR8OPVh9GSjYBbSCoy9rNniiaxc4eMRclyb2++3DQm3Hb54EGvZd qf93eI2fDNoN7uWO+fn8kKLKmx6ImaFla1sQ7X3AqKk5O7tkMp5R+QpSR4jdQXKuEc8V708EyRxF ++2ezXLFzcsrXj5/yXa11vHodL2Xec9YgtRXWxSgkUHGUBbEYt6eHotjo+D4dwWKvfHQK2r09Sik zK9kvxMFcVH/DmiPjfWG3XrDxb0N0/MzRtMJ9XikMgUt0TRvSZR4sM5/msftObeVnt9HDhapqQHV 39p/ROnEM9GdufzS7cspYEyZJVbHUlzPa1M3FZN6ROOqQrDoUt8htexlESrJwqy4ll0PInrkdRiH c6K8EynHqytRZkqpFNlGPCTH48v73L84Zx9bPnn+CT/85Ac47/jaX/sav/TLv8iTJ4959ORxrvwY 0XaByXRCTLDbbsBpg6ouJarcTK1rOyS7kW1hehGaZlxyRkx4HFulpdrA+Cl830NkmFOh4Z1+loas DLZYi7sPVKkkde2FoAyRqsyUgKTGIV1ku1xrjffNgtVyzWK9IorgpmOasxnVdAzjEXE0Jjglx5Gy KHJmNUKwOuZCKmOTrPdowsGAo3kqTAha5QxieSWHZbWVGwiDI8vo2Fo6tfgNwAyinb0ihUHjt9PX OLD6fkwLUFergQZ9RiU2E60ySIkQLRPcQMbQ6pGD3nOFtwFANPnTTxrqpuLt2Tvcu3ePJ08e88F3 3uP5x5+w63ZUXv2AMURCq3TL9Ug3YwgBy7coz20mGxRBXr4/HVfO3G0JnVJIrxPDP/Y0xSSloiuJ 0EXwkxH1fIZMJ4S6JjmHi5HKd9BGUmhLvlYiDUivMgggDfZOfrYCKA7nz0Urc6UYA+aVGJ6vylM9 IQW8JKjSoVtXz00FKPZBU3SNDyxN/crPU+A9iBkCi5/m8Trep5Nzm/etSxqi1hCUM3mPkMPRITId Tbl3fsGoGaNVQWqXKBeGAmE1GCISyRTiid1mrxUgz1+yuLphv9loonMmVexJQVRGGqA3EGEhEJ1X rQ2xhFJS6sE8ed6GwyCD2ZHBXOXfLdEXidkrQaELL8n10pd0W58nAxpSSlkTbRu42Ue2qw3T+YLz e5ec379kNB2XBnBa1pwU4zrdNyZrf9LjddeU2PMPvBzDNSlo1WjtPC4mYqceJctLtHs0b1ISesCQ G5RFtNcLAr6qmFYjxr4uXclJ6rVwoj/iHFEcyYKXlqdQkqKgAItiBQxdiLmJiXhXHiSFmI0ILU10 zlFXNS262b/0C1/gV//Gv8O9B5d85Zd/kbfeeYvJdJxbZXteLG/457/929y7vM83vv4Nnjx8wngy pu12hH2i8hX7/Z7ae0xxeXHsdzuDpD2XQybwSQMBJVnBiGi1QFE6A4vdAIgCi8GEvWKiLabPYELN ZSgR6ALtdsd2sebqxUuurxfs2j3JeWQ6ZTyfUs+nuOlEM+Urxy4/R0Xmx3B2DwdiEdt1kqVtsaiO hU4y06FXpLaxhv8WlJ/iLWv+VcetMEdKBx6EMo6mcOTws6cE6et87+scYtc6+g6XhZiGTYasqP05 ByNd9I/mC+1iR+WEi4eXTCZf5fLeBR+8+z7f/+j7XL+41jJYgVRZaWMsoMvadx+MG+bFOFQsd/3+ Y4/DCcBy8loZUMRoCY2OLkX8ZEwznyKjmtZr1UjlamqpqCTgO0GictKEEHrGzbKXhmWNQ9F2NNZD lHDXs2Sg4bD4e4b/0ldg+cHHzTBLJPzgurFczwTrwbfcHdoYGH5CvzZ+FiDj4GvvAJ3DI5FKZZ7d 1DALROVTpPGe8/mM6XisUiVoPx7jrQm50sKjlXjOwX7bsry+4frFFYuXN6wWS7rdvliuKYkqIhtt 0z9HHorSZBFTbJrEOfRmvMprEY/lzfHYiJaZk1BPuLUnH3gxhnlXB7IwJSJmcAmpDWzaDbv1jt1m y3674/zygsnZjGpU472W/mqVpOpEDqjLf7LjtaSfgaX8p8vj4JMUb0UljrGvECJt7Ihd0jSGRPHQ IhretHCjebdsrUiWS7UXxngaHG3oskqRHuSooi3e/8qQoAlgkwUy2KhGkjQ0sEAyMjS4qx/03uNF 2IUtKTqa+YiHDx7x9K1nPH3nTS7uXzA/m2lcP5eyLDdrvvlnf85/99//DzR1ze/8zr/ga1/9Gn/7 b/0mX/ryF5hPpxAjTeU1pu2Edr+Hqkaco6pruv3eoBy2t3TspTQPsyZE5qEQOXRDFyxyYsEOjxBC ji8p10HXtpByxYZ3hAhhHwgbZYdbXd2wWq5o245OBJnN8NMx9XyGn4y17XkhFNN2trY0o/RJVhqb zJm7A+tpCIoKIEgmZFJeeFLoxZ11Ecx/i/S5KCUmarN8JNDgdsz3eCP5o6qaW9c5ch7eusZR3sjx 8TpCXJXFMfgyweK1edxAwBVAlA7v1b5ryFARCdSjGomJ7X6PGzme/cLbnN2/5MHTx3zw7nf57JPn LK9vdH2KI7aR1Gk/nj7hdZi6Bq/xWLfH4hVAZHjO8e+nlJOIgiytMtVmdTELgvFsSj2bEmpP69R1 7RLU3uGo8A4kBtok7EIGbYP2AE7dBlj+FjCIAffgOJETMjPnxJ3Pnd/ywxeTWbmiIZrjj9uzF6B4 ePT6WA6+4+T5+bXI4VJNx+f9JY67AOCrcnEKnbzJtqGhlFJWFpH5bMKDywsmTY0n4SQRQpsT7bOB iCOFxG63YbvesLxacPXiJeubJd2+haAeDaJ6A21NWZ7TrZ9s7SqQMO+h/Z3KMxi4eB0FfXJtA0mC hjgGoMI5S+wVcJo/0Bs9ccAwKuSseX3PqTd7fbNiv92zuLrh/N5FT43gnTLIppRzVGwl/wwP27+D /S7onnKQkywTtTgayQT+CUIMhfZAjWFdsX3IUL3jqZQ5ankyCD4kaDvEV3gG4DyPexTVTck8FyUc ko++R0TKLrQcFrBJFAUVIfVNzEQcyalFFiURXcd41vDgjce8+YW3efr2My4f3ldeBgJVXatlI5Bi oHKO+WzGl7/0Nt/+1rf5wz/4hG/9m/f4/d/9I37j1/9d/pN//I8ZzWdayho0Mcxi1+bmDgJV5Wm7 Tu89A51TLc8R9QAcZyZYPHZITz2cwKJc86aPeYIbV+kUxUhsI5vFOhPGLNis17T7johQTcaMzub4 2ZTYVFBXdN7ItHJ6aIQGZS0MuXQ1ZkE5kBNlIQmU6GQJNqT+HEQKOKwGnoqD5zv82EkFZX+/jut9 yLJ37HLXpLHbAmF4T7fbJh193yveM2EqirpOnm9dfY1cK8aYK0oGYzD4UF4WAzeipjs75/CjGrpA EsfZw0u+NB/z8MljvvfB93j/3Xf57OPn7NtOS67FKf9+1pspUZTBQfiDV4OGY2BwF6g49fergEi5 rg6eAipJiHdM5lP8pKGVRKy0XLQLEZdDo+I1PKgWo6frOrp9S+qsJ6ytXylW0xBc2BinvN7jgVIc zMPw76PXbZKi6NyeXiiDMT5+67b74m4gMric/S5y+7yf1nEXWDx9jpTIBMV4SipcUqSuPfcuz7k4 m1KJhlCq3C9FUGuXJMSgsuz5p59x9fw5u9WWsGs1QTAkQmd8Mn24K4Rw2wsxABelcoP+nDiw9G1d GvD4ScamD6lkqzoTNBINaDhiEmSQl5Gy16L8LlKeS41SiLFlt+/Yr7dsV2vWixXnDy6ZX54zmU+V siFRCMB+Vocahb2BLFDyHUrWg6ixWzwyomI3JpQEMIP8oVcrZe+FlXOk/KJFNYhK7NjlTr/qNNQx 1kpOVyrAUkrquRhOTA82RG9goMwMFGv8WZBMAxpzfwDnHfW05uGT+zx++og333mLe4/u00zHWtvu IEUhphaX62K7rqORml/9ytf4L//Jf8a7773HH/3Jn/LNb36Tjz78iK/+4jvMJlO6XUvX7ZnPZnTd nlEzYrPf0Ywqdvu9ZkA7Dx5lUTQvilnpRWhSqH6BvrpgOMB3uBztlcp7Qhdy5rGWNnVdx2a9Zrvc snpxw3K5YrXfkipPdTalmY5hMiY1Y0JTawOlZA3BjNEOYukrou53SEg4nBvzVAwzF0oyWr5Tl117 ksGFd07pxl0vCOyBj92/dwkv9YwIbQz9Ij/ybAyTQ+8CIuLcwXvHiZ2v9FoMhMfJezzxeZ3PQwsZ MZKlvD4sPJJS7oeTv2/guTDVkZKQukAnsQekLmXSp5r7Tx8ymc84uzznvW99hx9+70fs15viQpcw 8AKa3Ddgk7KSGoKNO9bjrXG5A2C87rm9WziXpicdE19VjOcTpKnY0dG6Spk3c9JqylwHKXsFR0Fo cHTJEWiJwUiR0iGIyNrYSSwL2NbYABuir5x4jqM5KnwiIrcSQvU8Kc9ZrpfK/yg3YOyQJ7701mvp 8NdXj/yPf3zeXN72DppZ2NukNujq3o7MJlMuz+eMak3ilDy+PiPeFCLr5Yar5y9ZvMyNxdZbUhtI XRzwU+h4JTJhVZIiP2OKfY5FARcx64oh+Ig9/0ieiwHriz2kPWD/zIPXygjZ7wZKyfk0eX3F4sXI pc2mE/KPVY8gDpzXZpponhQhEz56hxdPt9nzcv8Zm82a3XbL5cMHTOczZZJVkqO/FNBMWTbcMoyS AfQs09C5KOAiv2jloLrP1KBLXo0AzINkYy427phC1E2dx0oNgKQ8ViFkjhIhJNDCODUqtOGheaWU Uv7w5uk9F06qPFm6YR3oTVaS2TcDMUV8JVycX/D06Rs8efaYJ28+ZHYxYzwbK5ohkSSUBxEkx/gc PtNN1aMJZ+98kTffeMrX//rf4Ps//CHvvfseb7/5jLqq2IeOpmnYt3sAvveDH/Dp88/46le/xmQ2 o0uRLgTEV1RNlUvhpCA8Q2GmWSwk4kgFX1kcXp/+DoGeXYBOHHihaztWqyU3Nzcsbhbs1jviLiK1 Z3x+DpMGmU+RcUOqa5LztDFqc8WYwxs5iUZDGYKte49Tt28aZF3n+YhQOice82lIRqnOebwTPJqk 5LJzX8zfkdQKz5OSuTKM65/DcEVuFS3iMAbEspAtkTVniId9S+g62jbQtjtCUMXicuwW5xFJeK+N g6qqpqoc3tfgUIr4WxZjwspwP3fTFgtUd6gYmmCgWBNK2iYgKMBIublLF49KYe3HFLLzdKEjiVKJ hxRpc06FVJ4UYXw24Z1f+AIPHjzg+x99n/e/8x6f/ugTwq7VxKecuZ0sNyeDeQvTnYynv9KSS2W0 ipL5HMvv9nfoeGkTIuWkCAKuqWmmE1xTKaiNoVBwa/ImqjQk08IltX6ruiE4T7vfayttA5GDRGQM 6MuwwsGVu+nd/IMeMnl6zdK+9QynhPLBbwNjYgAJJJk7uJhSZWQPjtTfw7Gz42cBMO467sqZKbl2 pB78QvmZjkfMx2MaURBYkyBorsx+u+Pm5YKXn12xvFrQ7nYqqzsNgTiUOyJmnorSu2hgICi3RCjJ jb2Hovdq6KQfvl+UnmRg9GN6Lkro/sTvxz8lifPohyjKIJ1QGvnsxfA5pJJC0lYTXohR2C7WfLpr uX55zdnFBWf3Lphfnik/05EMMzmUgxCHc3n8THcA6nK27Yss7zneC5akn/q95EWpIlKu/jhMFipQ HVvFYnuTHNISl3VUPkORi/a7y8BRukiqIfqKypBJ70pGa1fzCBtzZqme8IBEnEvUTcXZ+RlPnz3h rXfe5smTR0znU6pRBV6vF+1GhwowP0AsQjwv1hSovHB5Mefy4it85ctfwDtPDG0/5KIt1/+n//l/ 4Xd//w/5pV/6Bb7+jb/Jr33913j8xht4L4S4V+s4Rirv6PatJnGiXg0Thl5g37Y0zjNqGsKuLXHK QLbKktUwk5Wq+mx3ux2b1YblYsHyesl2vVH3XuXx986RyZhmomyasXKle1zK15CoY2BiNhrzUkbR 2vxMlHzGpTJ+puA1ma2nuTbA5Jw2oFOl6rLrKyPQZBZCzBVBGX2SES2CUUzoXpcCvFLqF5/LGiF2 ytWx22y17ft6R+gCcd8WwBFCp58VCzfpIjXadyfgvDYFq+oGX3vG8wl1U1GPRlRNlXMwtNhXk49i CXPFEHLjPadxf9FkNMhgSga8EimpMrewV1FsOdSVx1lqRyDqdULsT8IEmXKdRBmWz0ohY9D7E6pR zcWj+8wu5jx6+oTvvvs+3//gI64+e0ncB0i55CtqqTIIXciKO9+bCmsDHLnHR3Yti+3PLCDE6jLS YZdaBRq2evq1cuvIVotDATRNTXDQnM9pzs8gCVVyNFEVmMtWa7IxNqUhQiL3nfFeW5eHQLdvNdkz hCyk+jCMy+C6WGzD+1N/LdnRUW61QIMTVt6tp0vQq5t0dJ3DE487B8vh2+ThPvz79mj+TI+78pkM ZKqcE7qoZfu2XkbOMxZHk2CUHFWKSIhsN1tevHjO1ctrVjcr2s2O2ClAUK6E/MyJwnh6/BMt9JGU t0LDHbEPiRgAoc+z6A2YlPHGYZjk+DBgcPz8B+tdhnv1NrgABQ7D14bJ7coJ0hNLJgsTmXERgSBF x8Q20G1b2uWWxfMrphdzzu5fMjubMZ5Mcriwt/qso260cYCB7Lcp7LlvDuQXqeQx2ek92MjnSlZi 5KaSISGddi1GRPOZzKgsuUkD2WBer2TrTLJHRw3J2HU4X+OTet/DPrBarVhvtriqYnz/Anc5pfL5 Jocoz+dM0SiZMMoLiUDO7sT5xMMnD3nznTd5+uYbPHz8kLPzOc47tfpK3MVu9eQyObJKTXsm7azq helkRCLH8UTdZSLCcrXmW+++z5/9q2/z/gcf8bt/+Ke8884zfu3Xfo3f+q2/zy/94hfpOiXeartO y1zbrrhrBsY6k9EIFxPddqfUtNkSkzywkjSk4JKj6wLtbs96tWa5WHJzvWC73RKTUPmK0XQM0xGc z0mZotu4LnSDkXs1HKyHg6RSW1BlXCwJaYhk82IwG8zb5sgVMV5660zXbDoccStrMbCRG54pbV1C nUoO712pqHAp4Zw2rksxsVmuWLxcsFos2K02mlAbkir0pFa5lZZp/XcqVo4lTkUpW0Pv2ytF9HLk kErwzYjxdMRkNmM0HVM1DZI5OIbJa95VCg4ItG2Lr+qDjVKEjkvl1WTCzOYiX0sEtVKyd0bXy4Dk Z7ANh9PlbEHFPnNcFW+kno55+vYzpmdznr71jG//P9/m4+//gJvnV1RRGIl6QnzUvJggOQ9HKC78 mEv1Qu6VoJs+FPAn2YQRbidaHVolpyzC3sQSlFQoirAHdgLNZEIaNSRR0pw6Cj0jiW6WJLF4dihz K8V76G19dh3SdtqiPsSiWBQ8CF5XYRFqZimZJVpk7/EsHDobXpELerfn6xhUnDr+vwYRp467gQWo UUGelr4NgQM8QpPZlF0bYNey22xY3iy4un7Ji5dX7LY7UqteisJVEhmUHh56Gg49E0aclQbgon+v v8Pb1SHD57orUdye9/j1oRcin3Rw/klwccdrQFnH2pNE+weph/EQkBjHg5JVBbqQ6Hat9lS5umF2 Puf88pLz8zMmswlVUxNDfjZJOCdg3VhNBxvoBszotkwKl5PvDVQ4dG/1BmQuuScbwkk7/LqYlAY3 mkFgSZfGFQOlL1UOT8pAJ5l3T1BdLNJR44ldYrPZsFpu2W5aui7i6obOeSaTiqoSny2NfpLwaPYv AedV0JdeCxdnvPn2Ux49ecTjZ485O59n61LdRckdTu6PfYgK9ohAtGSwPpbkPNx/cJ//6B/9B/zK r/4q3/zmv+Rf/at/w//9ez/i47/4lL/2ta/y17/yFZbbdW7NLLQJUuVxrmbftlTO41Ki9p79bkft PbsQkLqiDYkYOlWkoPkObSTsO7Y3S24WC24WS9a7HdsQoK6opzP8fIKMR1BXUNfgHJ1tFJvAI+F/ +kgDc6qPqxVwaS59UfediOjzOFeyoeVYfKZeiQ++RWvAc2t2SwotxGKiPB2ld0tMhN2eXduyul6w WixZXS/Zb3dIrp1WjwaF/c1L3ni97srJUqY4zQpSxRm7jtRC2AT1IriKzciznkwYzadMplP8uGE2 n2lXzdwPhAH61/uX4WOXxFtbl8fCSQZ/229O1LMS80YMhCLwhL5M0eam/JutE3xexynQhhbnHLOL GdPZhMl0ygcXZ3z07gesrq5J266UVhaXRcg+P1PMZLa9ZKDy0CAYqr0hnJfsLes9UCdCLkcPkwTE edoUYDSmuZgjI6Xhj9mLUJraGVSTwbKV4ZrLgt8rAZ6vKrzvCF1HaDud8xhLiEPnKSPvaIJWbj3z AJrnOexH4C/HMvBX4VmR5WoAACAASURBVJACAhQsqsO5EkftKyqE/WbLy+WKzc0Ni+sblssFm81W 923SKhHzNhSQEM2LR++NOKoMiTEWMH6qakSPw4TOu8DFqeNVr5eQRxywk3IaYAwTyO2+jXBvGDop hImnvBxkThhJypGRK05IkW0M7LZbVtdLbs5mnF2cMz8/YzqdUI8aragT3dKkPoVSxFpwANnz3N8n CuRLflJP463rX727LstYl8Pt6n3q9YZ3XnV16sPtBztGB4+D3ZZSdiDGnKwdWa92LBZrdtuOFDSR W7pEd+0Jo5rKLiyZWRKJBFpcY6WbwmjScH7vgjffesazt55x78Elo8mIalQRCXRJSzE1SVMME/zk h7m+SDrAVj7qFKnVTc3Xv/51vvYrX+Mf/sN/wLe+9R1+//f/gM12w5feeQdCYlKNaEl0US2SXbvn /PySphmzW22ovGez3DIeKwumH0ELJOeQ5BXh7bUJz265ZrvaaFnpekt0IHXD9GyOn09hMiI2FW3l dTADuBBLL4vjpMfPf/wcE8wmdflE/mxJUnUJL1n5DNA0w+84UiQlDph1mOXExHDs3lfBIVlIhP2e xdWS1c2CzWpN2O7p2hYfE41ozoRLmrMxakZKquI9VeVxOcG23JKNiUixdjTLXBfjrt0rWEWTqdqb Jbv1mnVVIaOa1XzGeDplej6nGY3wPpO9mOJNQ9RtHjQOvr8PJ/WvDecokS0+ceD1fu1eS+Iqfdw/ 73ssl8fImsjEciF7c6RyPHr6hNlsytOnz/jBB9/jBx98yPXzl+y7gEtBnZnZlBHpp9OJlhHH2LMX Sk7asrp+Va3u4Il06G9XmZx8esmj5hxdCtTTCZPLc2Jd0YojSCxjrNaTHkGMnj4LNW5b2Mbm65zD V1p+3YnOcU+ilEsESTnhNoO5PJGGW4wz5NiP9PMAMD4vyVNBagbNWSYYS3Ajjna74/l6TVyt2C+X hK7LoEDXbDBL9wg0GAgvYYsj8DEEIQYugIP31ANlzckOgcStxO583DIG7nj+cv1k4bK78yuOr3Xq vJTSIcnW57yu8leTRyUI0Tu2bWC32XD14iXT6ZT5+Zz52ZzJdEozHlGPG+qsN4wAMqac+CrZWMz8 Ub1jRoqhipmS9l6+J4dT485cInkMtXKG3sNU5OChAWry81B3kT0pkXa/Z7lcslptIHoq12Q9EujW W3afOSrnbWD0+tFBl1qSd4ynEx49esg7X3yHp8+ecnnvgtFkrKvXJ80FyDessZucJZxjVHcdQ9fW 7YVj/9PDZ34KX1UqSNFSn9pXXF6eM5tNeeutt/mN3/ibLBZL3nrzTbZxj6sdbbvHJc/6ZsE/+2f/ LffvPeDv/b2/x5e++EXGkymjasxyvcQloRpN2Ow2uJgY4dmtNqWcdLVcsdu1OhFNzXg+w08muOmY 2NR0PpfP5RJHJ73FNpwg22B39TjpZzEqii1jIwXRigh15bPlr/XoZOXRVzYMFsTAj9Fbl65v1hV0 QackGsNHyZMqhC4mtquVeiluFqyul+xWKyq0u+G8qplUNeNmwmRU09RjqsplMh4Kp8iws2o/0dnp kIWRhY5EtMNoSIG2C7RBQx370LHvtEXyertj1yzYLhbMz8+U+3/c4ET7XUiKfc5CnouU0bgBAauu MUWJ/ZtPsHs1ITKMyRoQSkfgr59v7UCZUm5LnPeDfWdKML08Yzqbc3F5zvxizkff/YBPP/6U9fWC katxueuPz2XJXWsVOr1QGSa4mjcl65MjS+RQcNw6htOS8n7WFUg9mVBPJ+xSZE9AqpoUJTP49U4G KdbO8CuLNFSDQez+HT5n5bvKE9qgJW5tSxcjLnSaaGvPl7tkkj/fU1icBhY/70fKYMLGysWERzlJ avFsVxu26xVsttDu1fOUPUWQLf9wAhjki58CEQfhj6NwiV2n/Nj3DbwXds7tZ7n93i1ZMjhitCqH ASofXCsN3lNDIO8n82zaa0cA5BiYDGXCwQ+S66hz+LfySEh0+46bzY7V9Q0vmobJdMzsbK7ejNmM 0aihqmucdyRfEdxgXAZ9V0rbifxoloBsBpvkIok6c8tYk0kLP5p+LTr/hFBIxX2ZZVoxzszaUdqH 8WTMdtuy3exJSfA+lyPvhJB2VOIz3asJVAez8xlPnj3i6dNnPH78mAcPHzC1luelYUvsXVuknKBI flCT0q9/HEyms+GilJMq8VPmqvCOtm2JnQ6Y98J8PuX8fK4Nkrxjs9sxGY3Zb3b8yR/8Cf/8f/wd mrrm//jf/i+++rVf5Bvf+Abf+PVf5/LyAocjbDvq6Gm3Oz57+YLl9ZLFaknbdUq4VNc0kzH1dEY1 HWk+RaWWV7TFmg2pPhn2cGO8alOUZ4e+KmEAKqxLqYYaDJ0eXs8EwNBHcAwsdJGKrSJSUvr0ynpe xEjad6x3OxY3NyyvbtguV4S2o0rCRTPh4uycadMwHU+pK4eTigo0hCaCZFZKkcO1a2v5kIQLJBfd DpVmEVYpEZImh+5DRxsCu65l17Xsdx2b6wWpC4xz2GRcV4XQxgD7gbdmcC8GDIq1m3/xksl0jtbn LVCcOy4aHX1+YzDq+n0hZc+QkGlydcO7CuYPLvjy5Je4/+QBH333Qz567wOWn13R7jq9v5zn4nJd uYLXXP6X1aw1dNfxPaV2y0N8rufMHBcg1M2I2fyM0WTMJhMFaRMjCi+AgVW9m8yvkK9jTa/MS2VC 3CqyXOVzeCtmoSrQtqS2A4vLSy9YwUj7/ir7JX7y45ahhvV8QMmUXIXEpFTduxafqyGISfNfckVA CvFWSOOU5yKGeCsscpyLYfc1TNIs/xbg+PrAwv7+PO+FuFzIP/BmFMAhltN0O1xSZHQOkZzyThxf cxgqkZT5lcRpBV2MOSSoHYJpA227pdvs2CxW3Dx/yWg8YjKdMptpAmg9HSEjBRpuSKXAwBCC3itV DFn924sahz7l0nKjJB+AjLJCxEKOvXAUOQysi9w2Hpz3TKcTQpcgLdntAqHdau2RE+K2pQp0uEoY j8fMZnPO7p/z9lfe4tGTh1zeu0fTNIhIJgZJuUwnKRFQvrmCnrK1oUr/9Y7bcTHwlWXz94mK5ulN KRK6QF17uk5Lm7xzec0oMosxaDljCjSjhoePH/J3/73f5Jt/9uf80Z/9a/71u+/zv//eH/ONr/8e //U//a+oENY3C+Ku5flnn7FYb9h3LZ2DajyimY1xozFu3EDTsHdCEl+Up+CoESWWiRGcWc7p5OI8 1aBreDjpPUIKKHoPgBF4Fat7uKnK4rtr4x2iblVSOV8iCrvtnt1mxerFNavVgs1yBTExrUecXZ4x H0+Y1iNqX1F7R+UqrCZQnFWDxNxkLR54BIor24EzWoOYtOx26FWQRLvfZ1Cp8+qCgp9Rpa29uxhp Y6BNgS5FWhJxs6dFcOMxvvLZe6RKMDkjFepDFra+4FARn/IpDZk1RNAE5wSSXF5vMVt/GjuNOanZ GYurZMde7tbYth11VeGcJ3WBkZvwxuwZs4szHj5+yAf/9n2uPn3OzdU13b7DS/aAmFciWTmtehd0 Fu3+Uy+Bjp7i7pcG7yW0HA9hMpowbkY4HJX3iBO6qGGLlCnpfQbVZgxYea3lXkl2o/SgVkrISJz2 JZDM8NN4R6or4m5PDJ3+mGfk4LOHgvbYSfrzCD0OFHB2j5kl7RJUTvAp0W73dOs1abvFdx1VSroP M9BXHopUDA+r/Cgt0YdAIlqVX7r93ivAhb2mFrIB4jR8mN4yP/BYDN/uS8x7J0U/85Y7YdcYGga3 dc5hPkUCJISSID/0Zgy9HmWMRb09pZVEjL33Q0SbwYlxIuXXvdCFSNi17FZb1ldLbpoGX1dU4xH1 dEQzHjGeTGhGDXVTU1VVNrTJ92qklz2FlktaNquBhaTVbpaQnpKyEtvvokm+/RDnZ7GQCCqj4Vgi qkNCnGM+n1FXNYvFmuViRYitwtkIlR97zs/PePbsGW+/8zYP3njI7HJK1VSFIwKxcsxArlnsJzMv YF1sSo182OrrczYClEm1gfe+/14GwslQrtZLZ6tNlEmucV55Llz2ZgiFtOWrv/o1/sl//p/y3nc/ 4E+++ef88R/9Me995zuM6gYJiasXz7n6+FN2qw1tTITKU09nTKcTZNrAqFGK7srROQ0XOJeT2mJE QhpQrmovBpyCA3veV7n+yjiYZer6ChAt1+xdbub+ssk/2Hy2KbMSPdqyRSjHENQDgiN1iRQC2+2e 6xcvefniM7rNFmJgNh5zeXbBxWzOyFVU2cPhbW6yAJLMwx/RWnnL6c0euDKV9owpc2yot0eO5Eqi qiv93cJIAuK1vJGYqBG8rxhJRXRCmyJdSlpmsNeqC5wqOGU3VTroGNNJL8axbiX2I2fK8UBdiW1I FWxRcgJ0pjX2uZwtJR1rBteIKSJeCDFAFpDJJzyO+f1zxtMRjx894aP3P+Tdf/ttnn/6GbHVSi1t zKeNAs0qVSGdmw2mlAHUccqwKuYh0LWXezHSn+lyF1TvHGG3Z79Y40c1zbg+AFkZIqB1iuo3Ebtu 2bdaSmvrU3IiWzLIJnn/ox2JXaqhrul2W7a7ROxs/FSYx6R73lg9D5ePwC0x+PN3mAda95gCXyee FCKb5Yp2scS1Oxqb9+yBIIedh+DCYvMH3oiBN8M8Ffox82wcVodAf/7wOAiZDNLch3ljQ0BwLDqz E+L26xx+tozJ4PdbHt+hdyBb7ppTNfBYlLF1B+fe8migYMR7j4VOnbcu2gnrUluuFxKdBGKrnZPT cg1ecFVF1dTUdU0zbhiPR4xGI0bjEc2opso5bVWlnhFxOdfCC77KvE3Fu6SgwsJWQ+94v2cycDcg lk7ran0t91+pHDIZF6ywWm0IXavcSv/NP/0v0rM33+DJG084vzinGue4z8C9OsCbeaPHgjjNPaNf Z66ufNsiA7HVT3zKStRi8oWeO1/DVb6snJTSrcTIGCOVN+tUB7BY8ua2JUGmeU3m5o6wWa35+Pt/ wbvf/jbTZsIX3nqb3XrDarHAOc9oOuOj559xtV7yS1/7FS4f3acVkLpm3W6pmoaulAdRyJCccWAQ CX7gJh8scntC2xAGlvTZtAeF5lRk4SlgSXDIIJfCLlCU4GGMsXSMtblJNpsuK6SIT0IKkf16y+Lq hsXLK7brDalrmU+mzCYjzs/OmY7G1N5pjbTF2aVfkJZbkiST3qaY2e8YLN7D+bfXki2Eg/cBlxuJ JVVILgNaW4zRPi8QnSiHiK3YzKKXUIARM+lbcup2T94RckvvW3nHGQBYQqIwsLrLPPU8GuWJkhJv pRAzLf5AcIr9SL73Q1V4YIUnJbDy0bO6WfHpjz7mex9+yI8++iE3L14S9x21q3CBwrmSEiXB01lO UgrKyldu8dAfcxCuwELtQ+CqSTNx0pDOZvj7F9SP7uHvXeBmE6IRaIlYrW628PQZMuHI4dC6PtRV BPvQurbzErio7Ldt17Ldd+zbtgetgMPn/Ua+/xyaFStTtuDM6eMV6WB/JY6UkvKLBCU1GjvPzHvc vmXxySeE5YKGSI2yKPZYNK+TQbJfAQ/pNriw70rx6LWBpUyW9wa803Be0lDLDF+OB9ez465QyPHr QwBs8ja/cuAh7987Colkq+jY6D0FKE41f8ww4qCH1UGTTBleQ/OPhtfIZYC5gEFKAnRVV1RV1QMK 76iqirquqKuauvLU3jMfj7k4O6OuKuUByjlisWshxsIpo09OAZTDoHofPul/hpIr8z5nAKJCbrfb s1xuWC/37HYd1Td+8xvMZhMtJxVNlhwqhENb4FDSFiURTUnI0WSmk1vc+8M251b9cLRiyr/Hb1mS XTRWxGQlVwogLJYXScoGCaS2I+wDVRe5P5vz5WdvsVysefnypZbHnc8Yz2cEcfyvv/1/8i+/+ae8 +cUv8O//1t/nb/+dv8uua2mqRnkGDCZkEJUEguTcSMnPferBDWnHHjXafZexEMmegaNNU67NwcIo Lx65HG3QTGF462uRSV8WiwXLl9dsV2u6zR4HXIymzC5GzKZjmqZh3DSqaG1BFgCTBiyLkESFRsz3 nGK/gm4PwyCed4ewCPn6Wc31QiYvMzfwCBlxVVmaIaklJpByy+UUHclFYqXxZO/7DT60fBP0JY15 vAvAGIyvO7jv/loRdfOHrsvGXCpEZCkXc7i8oYoHJYOokuwpGsaZXJzx5mzMxcP7PHz8iA/f+4DP fvgxu/UG5xzdvoMYlJNGpIxJDCGD0uOxlVtArn9rKFgVgCXvlYtk39JdLei6QLXeIvMpk8t7yjjr PTHXFRupVUxJ6+rd0MqhAInj7y3LqthQCk6cc1R1xagOSNvStnul3A+xhIVCce+6bEiAQs+fx8BI f5iXwFiKvVOP32a1YrVYILtt4SKKA6PFuk1YJ8wCBrMyGuZhHIKJQ/Bh5w89tvkL7H+f68k9DqEM /70FJo48FH0YZCCy6MVNLx/TgX6x11WmHV7rrp/j71SZ0nsVDWAc529Azr9yiRDVC5CilrK6mA6S 4MVlw6UN4DuCQCvKx2N8RJXT3ETvhDCfM06Cm05VLsfY6xN77gM93quXOyH5UHwU4BHLp5x3jCcj vK+YjFrW6y3V5cMLkvIRoiQcSpIkA3h/a6MeQX9LBrHJES9lYQ0XhApBoarrQ7Q3mPnhlRM2IEfP aYslbwsRi/XmmLeDJJkFs1MGurDecfP8isXVDbvtjrYLiPcwHsFsTDWbILMxm9WGH1095weffMzz 6yt+42//JuPxiNWLFXU9UuvSUxYgZE9BuWP6hky2auMgHyJvTHt+bxUVllOB5Ek7TAwcKmLJbqm7 kLzLCZJmQVRVRY1jt9+yXq1YvrwqXBU+wXw04Xw+52x+wXQ8ospsbC5pCEqfRb0yiXSokIWSdJcG q/MnFe8puw0zbisJgqQeVzvRRW0Wr9ADNvt2JLtn83vBkVn1BKmrEjYx0o0yhzK05HRNmR/DNuqx W5WUmwR5vUibBbGW9+o9SMxCiLuTKq1KI2Z6d8Rz8eiS2dmUi/v3+OBb7/K9Dz7i5vkVrhYq8UjU NtceDTloCswhILeRK8uxICfbcwNg4YSuUi+Pc5mefttCWBG2HfFqTbVN1JdzqvkUxjVBhG7gOZKs sESkJ8RKlmA7EMJDwCF2JwAuC3fNSndNje9q2u2OsGuJbafcDb4vuTXvxSmw/fN2iBNi7PBU1M5T IcR2z+r6hu16Qx21UVmX99owjC0CkqICRPpcjGGehe3HIvOPZL16Pk4BhGwdDzwYp/bCsefpGFS8 KrR8/N7BGjvx+jF4GHp81aiIfb8RI3IsRD3aTwMDx3YNp55SQfuVDJugDfVeIqrhIzrGzjlNkjb/ bv4OnCjLpkuIB2sHr7ehIfgkiSgdIkIrnna2ZVI3pXTdQFQkFVn6qqMPQ90x1jm/MWWCRO0IC/XI UdUj6rGj6mLbs4WJxb6kbHM4ZfD0iO1gwnS4ToOKgRIdNhOzCS2LYAA1h7EuS5DU57IFoXkXEnSh u1zDa2Km23fsliu2Nxu2izWrxYrtdoera+r5nPF0oq7fcUUa12ydwGTEf/gf/yP+1m/+HT787nf5 5a/+Mvu2xfsKJBOQ2P3YT5mCIsZV4eX7Pc5AqaqqeCv8sFRTl+OBXDy1MW6FGo6QOzFROa/utRSI u5bFasP11QsWV9d0my0O4XIy5Xw652w6Y1Q3OWHIaRMvY5okIc73lrV9Z7G8B8sva+ckriD3H/co gsusWsyqH/xtyHnAm3+Qw5jHoHiRguYDuSgEEVJsFWR4R/IO8ToPyShuh6CmjPnh34e5Lvqvy4Rh dVUrd4cE/XewL3SdSlG0Q5xum7kLQUuvc9lrNRnxxttPGY9H3H/8iI/ee59P/+JTVjcLbY3tPV1M ELvbi83G4+jvodVl7JjW3CigibaW++NxKBNdR2gT+/icbrtjdL+lPp/hxg115QguJ76llAF/VgbZ AjhYKwzWtu17MSMFVUBZsLrKU1cqN2K9Z7/aIl1AQj7P1oNZZz/HwAJsulVKeNTA2i61i2dquyxT VUZGB5JlfkJj/pKJsI7DHwfeibzHjo1IO+84D8OOXnlz8JnD8w5XymkAcvu1u4ytUwBj+NrJsMfw 3N46QLVLDvvm1+xOynWSlvr3IKIPkxzsuzwPYp1Fk+VjpCz0nDaTs5amxhlYrCwjT7QqNMFJIm5b 2vWWNJtRVdpkMMZYelFpB6BTev14PO/YSlmuInrvhBxFiD24r2qQP/vDf1F8JOZmFvFYw5PT33qI JMlDrv+lIkyOQcWwW+ZwoEv1hE3wEKkaWqVfUGZZlvlNebBEc0UW2w3b7Y515mbYrrcazqga3LjB Tyb42RgZjegq0aQ/Q60xUUctQ7q6vubho0fEGOlixDd14Yc4KO3shwTQDHqXDKf3qNXnRVvVdd9Y rYAKu5CZzSc2TzKPSD83Q6Iai/e5qNlcXduyWa64eXnF6mZBt9/ik3A2mzGpR8ynUybjsSYDZcWn i6VnvLT7i6KtmS2X5EhkDO9SqyhOLJtbz3PKokBDc4U08mgYFFzkME22vu0qJbxggBV6CwlyyAG1 sh05D0PBhVS5jbD3JOcJMhCYdm9FF5oLIJV7OnwuDtz2IUW6GAhGjyw+tyY+Frz9GFhmecr5RMaS mrrEi08/43vf/YiP3v+Al5++oN3scSkDuhBxhyuqQF47+qpx22/mftUclZ0XUuVpXE3tPT55fXjn COIItaerPWk6or6YMbp3TnM21T46hfbcFaDWx5FzFr8bWspy4AJWt3oCyeDCQ27liItKTseuo93s 2a83dLu9znX2RKl4E0h3r8C/+jkXURk5ozCKHtnuuPn4E5bPn+O7jrFALSm3CsifKWZRlqUpZvrI IxAwlDcDj8bwvL67wCFouJVzY57Qo8+/ClwMvRd3gYmy+o9AwtBYxVR+NlwOPOTHQOPEzzDH4tR5 SK7kcOZdt/cyfbddJ4MC61btRLmLHK7XGaUXkxRjVOkDHN55qlyVpp5v8F64vDjj8bM3mF2cEYil yAAiKfQcNYYTzOtn+WwDrWsw9PY4i66ZZJ50rC2BGhdVyGQpXmxKBJK7yxmCuUMgly3d/sbCd3AM Kg5Q262b5OCccqQ+b6Ofe1cyXn2mSO3als12xXaz4+r6htVqzXazJSXwzYhqNqaaTnDTEbGu6SoH XhuKkUBipKLSXDQn0AUePX6MkqAnfFMRYqCNIfNY6B0NE9EUteZ7ThwsHuccVR4Hn93XDCbSPisi hBPjKtJ/19ArNARsKakXp9tuWV0vuLm+Zr1c0a63eHFczGbcOz9nMhpTO00GKq7AFDBtUPqfiOQQ hdZKq+QZNPBJlOc4QPrDOfucOb/1nKAJoalfzpYEadcVsd4Wg6znwfckA0ED5V9KeGOm7Y2J4ELu zqvzjXekKiIN2b0vh4LvWAnaM9xhnfVJyg6JDolaTRMiGtO+NTb5c5Jbv4eQr5MJ2kRzGy7feMjZ 5QXP3n6LD999nw/f+5DF1U3uW4BSEd8a1eFfYotO/3aHZ1YIKQlOM3SJkms7YiKJJgOHNtItOrr9 nv1my/h8zvhiTjObEocgI8aSP9OD6SNYdSAbUB+VCOIZVL+kvHeEZlwzqyrSqGG/27Hb7di2e9rQ acNBKhSV/JweKRW2ZGJkt9mwXmipfWXN7VTMF6AZbc7RuXHc7bmwv4tiOnq95HCcAB5DgFHg/y3z +DYoOf2Yh68f7iM50B0nPyv9qeVj0JPd3vG5U+GU2z85PHnLQ9KXyDrnNBFeckm25MRz1AOReWpx JGJ0WjZKRPuBOc3xMuUv1jBLiDGwWa1ZL5eMJiNcU6Pi0KxTfVaBQoI3eHyON2jRBSdBnjkGQDJ/ j/1dSeoTCa05khozp4Y3IZJKbH1oARTrw7nSOOo40/ZU7OwW6EgZUQ5cxoaMTimxrmvpdh3LxYLF zQ3rxYbtbk+XgKqimc2oz2bIZEyqKvVUWMkVolUZSZAU1J3lEtElOklUeeL2oaP2jhAjo7qhC23v RTCda5OQcjWDk8JPUfIq0EVn7mIDEwyuk8pr5rw6MQtHk6zj0LHb7QibPYtPXrJdrtjvdox8zcXF BfPxhNl4zKgeZfZD+z5zAxmFdsBLhXbbzOAikWN6aHJYN6hyGdy7rcloyotDRD+c++Pfy2sZJOsI GGaOuZPtYP2kHISL/YK274xCTi6l93yk3CMlpVzTkwlmJPUhoKCVRwGItVePxkAI2jPqxkz9M9Jb cjDYF/kezAox7gu6TEJEHttyut5w7AJNXUHtlcQtBE2Q9I4oUcuTJzUPnz1mfn7G5f0HvP/e+3z2 F5+wX25hnxjcXrl6sQez5WRAbWinCFDHvA+rXPqN9NUhAkQtt3U42l1Hu7+hW22Imx3pfoc7n8O4 wVU+C8GBsrB1cGvm7VAZI5LtprIFYrECXRLm0xHTixHExGq94uXNFYvVkk1rYcWh1vj5Opwoz0GF kLo965sl+/WWKiTLxiqbNbqh9TkA8hm43wIXMQM/+ph86ZicjY3iNUyJdGQoHXovjqpHjs47/nuo Q4bHUJ+86niVt+PQWJBiQJ8CMOYtPg5z9N4IOfBOmAfe3stvKvAeVFFZoUJyGkqJCD7nebiscFOU IhXVlZc9oeJzS/WEk0i727NaLJnMZkyyV9ZA2+f257MlMjBAEkNQpUDCyLn6McwGal4blSBKoiLq znZZkN91B7roBgIxD5qVk0rmZhi+dzA59ArRkOUx+DBXVRkMe+Ich0gpkUKk3e1YLVYsF0vWiyW7 7Y4QoapHzGdzqvmE2FSEuiLUnuB9voTTZx40RnKDBl5djMio0qRPhKppyvOEELQ8cqBQXTKgoJPh fFW8CiWnwsaZSEqupzceMEwVF6FwmyGyP61XcHkM23bPerni+lrZNNNqSy2ei8mM+WTC2XTKdDTS xVcSEvv57PNdf2jq3AAAIABJREFUrDRQ9XXMrsMeiFituvSg4ghkpQxAzB1+y134OcdAL5frGRGV rbmekCf13gx6dVLilhmEUO5YEw1jjKVqw5gvNb1E5yDsOyRB1TjEa7LiIJxYPHblaQbjkBcKpt9s PgV1Y2ozIiVb65ImfcZoYF5dppX3hNARA9q2vPI5HKdes2TVGM4xPpvyzle+xMXDe3z03Y/46N3v svj4JakLBUkMwYv0t3zbi5AH3NsfeVySyyBMTLGoVeuiMBKHB7pdS/vymrDd4tcb6os5k/M5dTOi c+qNs7ydnlGtH7OYxw3U2yd5cfk8jV4ctQiNwOV4yrxumFQNta/oLmbcO5vw6YvnvLi5Ybne00WD hP23JFtQ9vzlf/ZPVqSvsU7//3q4BFXSFvcO2G12rG9uCPuWuqzxaAzVqkjLGlFbeZhsWWRyHsxU rkHZWwUwFAPJXr9dElxATMzx7MH7p0DCqzwYrwsq7Jxjw+bOz6VIiofnwiGYsGc4BS4QJbgie4OG rw95M2JR1lKup83TctKmOCz3KIkSc0VxmqCbq6Ni1EpFFUIqd70kUmhZXl0zm2tHad9UGehDTyUB hwKg1y2352UAQM0jnHVYL2YPAX0lhV43v1H6Fcgh1agturzAxAnOywBQ6L89PXCu1RWK61rJbwzd gjEmIrEw8dl3VM4rPsulZ140lNC1HZvVhuViyeJmwXazpe20VXhVN9STKaOLc6gbUu2z68/1m0mH CStXy9MK9BUDks2moaKzxaWeFXdoFSN5PDT2JT6DCbFqhpQ/l0M5Wh+pPzbWdnNO2VALAVfOxi39 8VKkwhFDoN3s2a5WLK9uWK+UotuTuJydM24aJuMpo1FNnYV1ylrQrEIwpaY/RaGnVL7fqobK/2M/ j1q7rp/2ZVNpCWEZS/LCG3znq45EVvj90FLAjL1kQ4FlWltdfb+ODD0XC20w03ahVG4ul7zGRJX0 pJg5AqgrvFeymIiGKwrFtUhhzXT0zKOd4WDJXrGIjk3+Lu8cnYCP0KIt7gNGp6/3VsDMgHMjb5W8 /5TjIlXQzMc8mj6mmY958OQBP/zOh3z8vR/x8sULBMeoqghtJHYdta8gPwe5NXwROvpQuh5dn62i YPoweY2YSHSIc5k8DtK+I4VAt90jNyu6+1vcvQv8bEKstI17JMedbU9kts/khCCRFLtMpgUuRhyJ xjlmTcNZVTMWx+V4zMg5rZBJLXhhfjblshYW5+f88MU1z29WrDZbQkqIr0gmjAGDT0i/JyFoCaYI MTnS5xAB2ud/VsehmB6Ad7i1l2xvFqMnRBoq2O9ZXl2zW62REDRROZNqBSjguxDZiV3PQIXJ+3QE KIbfngaGTi+/y3UY3PeRF8TWW7mSKahkAOfV42tW9ODGC3j9PI/Isdf84L2QMojOIKQYV5l5s3yl mIbtraHs4TUPdJGjYm0chp+Rfr/pl2XDx3hkoiagS8K4mrwjg4xEkFRAuJVjgwIbiYl2s+fm+RWj 8Zize+daVULq14Hr9VwxPPLYi6GM1GuGEgI/wt79kjAMoS9URdAay57AgTu+WIZSkiadF8TnFt+5 Jj3PhE5G7G9miIttL1rdbVlLthTs82TrMKlLuRJH7CKblVZ83FwvWK1WbHZ7oqinopmMGU0nVNMZ 3aghyIDyOT9KnyNwCNVUn5goxW6yt/OOEW7q71WgdCVVTnl93v+XuzfrkWVJzsQ+c/fIPavOObcv u9XdHC5DYIYgB8IQWkBIhP6a/pEw86Q3AfMgzIsEARoIkLjMTJPsZvddTuUem7vpwczcPSKz6pzb fdkUFPfWqazMyIhwd3Ozz/aMaCGFkWQTs24AVoI1y41q4Kq9OlJgpVkbAFsmFCgCfd/iejzj/M0R t9MZqR+wcB7vVlus1wvsNEo4+AByBNYmWoBo7LmQkxKBEYvRjJk5DWShJj47jyurBlEGmNa5lghq ERPGw3o7JtWPqPw9/53cveZIs7+y1kKMlHQDsqusK7q2E2CjGsgDa5ouOoiBRRSTX4pRQlEaLzVM vKAI50UYF3oqvNLojWdXplTtKpKNJx0KGQMJg4+2F5gmQY/M8/Hr+yQMMVGCC4Ttux2WqyXePT3h 3Rcf8Nd/+Vf46he/wrW/oXENwsIbsYuLTi1MrCWDocqB+eNt+amyVonJ1imT1Z5EqHp+RIKPEak/ o+16DG2PxRfv4Z638EtJjWMTKiTjzfE8JJlUNI6glNAQsCBg7z2emwbPyyU23mMJwGPMQTnmvlut Ftg0DdarFTbrI3719bc4XC4Y0iCp6RS03TSLdghNIXa6N3M3yhxJ9WDW71bhM875bsf8ilp0OMsz LuI6H6SWA5cAn6TUd3vp0J0vSMMAn2LJMkAJ0SOGuhaR93J2ZUwsF7UJvFB4NrPXwAHTmUsV/7yz FvD8/c+Zz2JdQT0XRlcPLlHHScytGA9dLfWTMBcQZK91r9ZWiQwWJn+WZAWzzD2y6LKeI+XHtSGc ghMygOGcVMEm5btqUSQFGmZ5Tkm3RhpxPZxx2RyxXq0QNkutAsMSvG5zycIPHCD7n2fuLEO0tQyg yqXL9VpAmXlCuF80g11l0iUTRgZq1cJEoFBJo8kCSh7aGd7Jbg3kLAtoypoxF04WbGdBW+K2QEzg IeHWCqg4HY64XG7oBmEWfrVCs14hbDbw6wXSokHvfdYcq5n55FGRv+2/fMwBiQVpWiyFN4JxFsok erL9JUyhEmHMuR7AZEvZKVHLtbK2VCcPHiLa6w3d5Yrr8SSlyq8dAoCn9Qb77Rb79QaLpoELNTgs 7Y2FIF2O+M43nWlgnFG5/HbAhHnY5nSugIqpybBYqIyBWQQJQ3zmCVpH48HvT60ZVaqbrIPP7hFm KaU7j1TP3yUqQmnGYOa1XVxixDECnEBJ6N55gdnRKaWQzI1ZajIUV5oxfk5zHgioBUvCtEYmycZJ mo1UPXqdhl0NJF+LVSD64OHWHpvFGpvNFk/vnvGz//Sf8Yu//XucDkcMQ0LQcvIZhLEW4/Feg3WR tTMCadYTQcugigDLFkcZfzabUgEaMSWMtwEjHxGHhHDr0Dzt4bcrsSiGkvobwFKoLRE8JYQUsQoe 28UC20WDXQjYNQ3W3qEB4FMslhSGWIeU4S6Dw3vtxbBbLfD1yws+Ho+4dD36NEAAtpSNN+CeTJOu eONjEpyjvO8fWLx22CPNVCD5Kwt4VXbgMLYdLseTVt2NBQBoTQTFUpMLSxo8Zd5T3WCi8WeFxKwV M2CRrRzV802eda76Ts7h2c+jY+q6uQcKtSo0ve8jy8Wd6/7BeY+u9eg62eXspp9bQPhr4KK+r9nV hceyAhaTy0KDuaaNgyqz9hmpIUDWsb22+PjNt/CLgGf3HmG5AEQtAMDZYSG1R4rFnW3fy5NN52ey Oq8AcCaEu4XJ5i8UU47DRIBkDZVMlKYJHThyE3k1RTeKjqAkUhGDlNog0MBI/YDu1uJyvOJ0OOB8 umCIEc4HhNUqtzx36xXiUjqU9iTaZkMhu2J+06P40in/LbXcSxbIJDATU1AxIUyiifAsHkk1RbGW jk2cG8qkGDF0La7nMw4fX3A9nsHjiJVv8H63w9Nmi81qg0UICJqK+9Zmngope38mySrBZci2DGFW 6nYWtFvPmQAJGbcFvBpduDd+fypVsB6TsZHsJzf0Di1kVVcVtO8+uKZtqMl7rKmPEk0KRAYHJ6Wn vfpMrcQ4qhTP2T3y2xNSkHn2ZGCW4JiknF3SGAcDcjClTPebdqjMWk01PiKAPWGxW+Mnf/C7+PDD L/Hjn/4E//Gv/wb/8He/QHe+QWwPBN/4DLbGOEggrfOgVFJlM8wwAGHvmBpMZk6VH3bAyAnkHRY+ gEdGfDmjvbQYzzeE5y1W759A6wVoGbIJ1ydGYEYgxn61xHa9xG61xGbRYEEOC7CWBR+rYHPO6eCs dEsOcGnEfhmw/Z33eLdb4ZvDCl9/PODlfMW57aRgoAtgJ+aZZECLpAvzqwQ41zp+S4fpAHnUJhCq RyJW15yuT9u2uBwO6K83eE5Z0VOkCgCq9SK7RUwAWo0Lu/6jbI5k/Skynnh7f83Pee2z6ahfmevZ DebXVdtxOd2sAzNQUH//cwDGa+UT5s9CjrKiPLWYaBzbK8Ai/82qoJv10qwezCJrnZP971jcfhr3 IIxXEgesjvQ4jji+HLNS//7LL+CXAZxGTMGy8JBoGMhR2eOfOO7BosiMcHei/mvNqCQokSYd4ghF Y5HpdxkVC6HL0hZtzqwZeu0oWpoQIhVLBgAeE7rjBZfDCYfDAddri2EYwM4jbDdYbLYIG6lRkRqP 3hFGbz5blxH5p6fkjcniMhekwrH2mYUQcuZH8VWViXa1Gc2mm8p2IWv+ljgrTCZgiIEA6QswdB3O xyNOHz/ier4gjSMCCE/bJ2xXa+y2GywXS3hlGKw+4wl8qAg4qXvkjhgmzLQCFra5MuLW8YWQ5+YR cCHT3Hl+1U8fjyxONP9Q0S+Z2K32CEPdVCyxLYm0dbJmQjFz3uB3flk1LxjglVxwWc+STSK/EaQ+ RgoOgJcKoMIX4BJlsDQdU30v5PuXhFXNZ/fCNHIztMSmCOljltFSfm6Zf4sJIUfwq4D96hmr9Qqb 5z2e37/HL//+F/j4zbforh2G2GPhpV+Bi3I9753OExVrpO11hqSAa10YKcNte0DozCwfrMDMk7iq xjggpRO6rgWliOZpg7BbwjceHoQlEfaLBfarFXbbBVaLgIXGVbgksUQewmOKQRd53MwKfJjhWeMn nEdYB6zCM57WC3z1csBXH494ObcYUkKiBnDiKrFqi8XC+0iw/faBxfzO2YrFor2Ci3vS6b7jYUB3 uaK9XMGjVDN1UFChgZTpAR8QK6MomPbzeMQV8J2BC3vWT+33twFGXoTq75ofzIQePbZSzq87Bwz2 +5GrpP7ua4DktXM5cd4zJjfAprcVZSADChurytdpAw7k962suERnOW0bXwI6WX3nEU5rEhEoOHAa cPx4gG88lkvpcO2tyihDXMuZG0WA3f0cPzjMmPLgExA5hHri848LYqnwM0RVvjsVYgqtcyBITZRU wIURb1LN3JH0/RjHiK5rcb212n72gO4quevkPRb7vdSoWC1ByyXQeIxeguJ6DeITvkOQAmC/PhOY ZD+Qgiwq2TB52istvUx2VfeimpoaWMibuiosgZBOwVlKCTxGXM4nDLcO19MZl/MZQ9cieI+9gYrN Fo1aT5AATmLkssJLjqbarE1Hvblmo86vjHnl7a2EPa9ZAiC3463Hb4Ou05XrmAqjyNdiLmAmvodP R3fvc5o9KyrtibS7LJHmksucDCm9yjTrdS35+vpxAjgljDHBRQcOXp4duukVAVgmUT2n5Q7Iloh6 LfLzQqwiHlLnYiSaWF8MPOV1UiBbM8iYIkJowOQwjCOwIHzxox9gs13jRz/9MX7+s7/FV7/8Ci9f f4ux6xE5CmDQOhZEvpidYXULUgEYLGCHibXJm7l/tGKu11otMQLO5xLU/RiRrgOuv+rhryts3u2w fLfH826Ld5uNZDctGwRPCF4L0cUIThFELFYORxILzYzS1rpaP6iOE0dgHOAB7IPD+mmDVeOxW6/x Dx9PeLl0OLcD+nEAfIAjnysZklmT/gnBxFuHbKPK/40CLBCTAIvzGanv4FhrAbFy4SSxXURQ9yjN iJXzPpzcc8I3CrB4TeB+Kq7hLTDwyfE/+O5EPj249OdZSz7/fnM3y1tHYt1DyofqZyYyaM6Tvyeu S1PqVcmLLCBagD5DIsYlTZ3JwREjqowJFOC8Q+xHHL85oPENkBJ2Tzu4hXQijixZkWSuXpXnnz7u zylzQmK5qE0+zjnNT5+abOpJNa228FwFKKrtJCrYy8x1DDULkVoqGEBKiF2P7nzF+eWEy+GIa9uh Twy3aLB4esJiu4ZfLcChyS3PkyONntWALJD0EeECDj7neKScEMRd5sniCdR6Q1VMgfUKme3L/Pfs ujkP3GRv0oZrUObMCeMwort1GK4tXr7+CsOtQxoGNKHBD/bP2G422K5WaHwj3zOfHpL0tNBnixyz NKsRMmCpVMA893zyrNVAHO5Bhas2lFmbRJGtAYqhGS7GBhKNg50xuPL+/Pd8/QpQ5ckb+Vnn59np r9BvzNYJ08Dqy1h/l+KSsGqqnAThe4akVmqGiuMEBA8KYma/BxZFVDEhF/ea62fmEpIiZlpt1SXE lLT4lugXQmNc6n7YNWw9vEPihMTaFyZICe1t84TN8x5P75/w0+MJL199ja9++TW++eVXOB+O4DGB nAgc2a+sGpNuYhiQSdmaVdA4YLFGOaDZi0adEJGS1OpoQKC+w6ID9rzFl8slfufpGU97BcxguDjC R86Bt5ZazEnScWtLjfAp1eA19iKlMYMMOVH6Ju3XSyxXK2z2T/jVywk///qAj6crhqStro2JFxsj vtuhjPk3zCJ57a4PLWDK85zSVRpGXE8n3I4n8DBKXozSacla0y8yMj0RicAy8PgWuDDrRp0Zcnfe jPc8us7nH/ccYXJt1kfOvON1UHN35VesFhMFVedGLzi5w4T/5++i7AmSGZ67Q6qhyHhQwIYU0avc KWa10AxMItImmeqa0n0o+yUhugBPQWIyxlJ6oL/c8M0vv0IaR6ThB9i922OxWpb1d2IgeIAty6PO gehsaaj6J8zLc8/jKeYLYZNt17BYgSw89OGcMaLKEsCwyouEvpfaDJfDEbeXE8ZrBx4T2DsJ/Fov sdis4ZcNODgxbztztWjAifq1BFyUGfmUz34yEQ/eC24asGlosvZrE5mlxObZNpxduJZYBWgBKowV eSKOGIYB19MZh2+PuJ1O4HFAII/9eovn/R5Pmw0aHzLjtvLOsSqfS0pgQsri7TKfXdZ6P3NfkwJA KwJWWytsE9QT+KiPSFIrVg0a7Ec2jNLP7Dc9Qmf5MNBijgS9j1pEKD/f/TfrufDwWgDGgl0NYKg5 X4WEjcmsVfY6P0pUi0JiqWWTGLAy4pT5hhTtqp7JXmfrmM270o8VKpJ4Hi3Kk7+cSnqyzaNZquw6 xGBIXiw7yuO04Nvt+z12zzt8+MEHfPidL/HtD7/Ey9ff4PDtAefzGbdbV9qwK9OphTiT+R0NXKhP yBhqihrsLVVwGUBEBEiUgffPT/jwxTv8zo9+iA9ffsBqswIcgzGACOICkYmAWSPUtCJamxUbqtyK YFNYdJWoaOASuJngvcMiNHi3XCOSRxuBEcD52mOIutaZaH8NgDHhvr8ZwHjrKL55oTmJ5SMgSsfj 7nJFf7sCMYlLmxO0aIpe4MHjqSXVQEMexcyNUM598H79OfCqDPlNAcZdjEUNMoApT8b960fPNTnm lttKoc6yr3q/XnHClGr4AXio16/+zI7E5uoyy5z9QPmCgn9IjRwiJ2usJffHcYR3hMaalg0SJwgQ xluPl28+5oaWi9CAvMlohdV855gpU1O9pkd0BIbB89Boh9LaemEV9WqBkmeq0oIBTZdEEg0838zy 2Fi7wklwSewjhrZDe21xOZ1xOB5xvbaIYDSLBZr9Fn61hN/vkYJD9F78SZTryuWNZPix+IYYUrZc tbva/DQBoZSBgB2uEqKOCMHVVy5E4Cp0X0zBqu2Q7U+BVtJ9roAPqGnMNIQ0jhiuHW7nM66HE66X C1InTYW+eH6P1XKJ9XIpDcX04gRjfio88spwBnU51fO1DQghxgI2yudEDt4TfFjk9ul5jDX5ZKFb 7o4KaI0pgZ1WCtCo/ETIHRUdmdBD5SHSWIHJxlf4OkOLZBWwJhYQLhVwUY+V7uZBQCPALI3B6sBP AxJlZrXaJ0SKifww87vQEUUWa1RPSJ6A1SK3DWcCotGas+exfVWsIwaw7K6U6UfAcwgeTotRDeOo mpqKAaUHEjJTDUZdkVw62QIEKTfM8IGw3K3x5WqBL37nC3TXG44vJ5wORxxeDrhdW5zPZ1wuF/Td gDGNNgBhZmqKlWqaSZmhWbeEIkaWGh4IDsv1Ek9PT3jebfHPfvIT7Hc7rNdLNKsGcIzBJQwulvTx PBlc/eu0Yqem7ml1SYlVKrFPDJe7pJpVTVxjsrrOOfgmYLVe4int4Zoebdej60YM/SDzT0bTtYC6 Z7iP9tf3fcxva/SSx5YYHkAaIq6HI9rTBRiitGVJDGd0rbTGaRqUZmZsVPQIA6wTUAEgv3cvuO17 8/fvxvNZ4KIe9Px+8nkBEcCj58lXmlkL3nJz2J0JRVbUFtBPPTurNm/KSv0Mk+EofdUgJluYqcQw 1eBC3BYOMXLRxHI+PKmJ1SHBYbQqogCYo4B2J8p+bAccvvkI5oS+7/H8xXuE1UJluchrG4tRhmVC ZjnzytzVR5hXHbs7bKZsTvRiMcZMTI1WpIxjnDQmYuGsSGlE3/a4Hs44H064HE+IMWEE4BYLhO0a zVayP7hpEB2VgBV7DFsPIBdyytpTHuyUgGpiycOpF7NaQIupkC6Q9wREZAWTXAYvPgtpnjEZhR6c tNqaABiOEXGIGLWp2unjAd31ChoTVs0CT+8+YLVeo1k0aIJD4wO8bfJMUMjaRUkZnS545DhJqyzP Veam7JHK9aHpiM57TMhnNh1VCNKD68tzFmXCgpvEXA9HSKMEllrBq6ggwSvytgpwXgvHzI8slPMG 0w88CnN86zDQNLu2Mdm6fbtZWeqrakN0EEsHCx+LthcjMBAhBZ8bonkrLJcYERLTYM+RTcu1daKG NhWQEyuCxClFTgUUIYtb3Secvy6xEZSV8UQSkGnl2+AdQligWS2wetrhi/4HGPsBQ9fjer3icr3i ernh1t7QtQOGYUDbdgLKRmSGJD5bDyJgETyaZYNms0JYr7DYrLDdbfH89IT9Zo39ciWxMA4gjLrP BVAJQ/NVUJsDKFX07YpwsTXSqHaL3QE5RLAGVhv9CY0n5zDGhFGLSjWLBmuI2yj4AZ13GIa+inMp dFKxwgl9/DaPrEGzzIBjwDPBpYTu1uL88QXD7Qqnzf2yoOQCkOUSPLGaFTcHT4DF1B2Cuwm4E7av CO5Xx/Pgs8f78tE1pgD0rXu8tk6vBWzWwOJRfMWjmJLXXEAP783GGx/xt/u/GcoXc90MAyS6d8ga pEnoQeQIaEEu5wg0RuHtLH+PPODl64/o+wG3W4v3X36BzXaD4F3OnJIeYSLvLBi+GBzu6WAiAwAE ci4zRlECVBsziTCbaD2tXCgxkvosxYQropkTAUNCf7nhdjzjejjjer6g6wdEQGpU7NZw2w14LdU0 x+AklqKyIc8wKGxmi2JTE4TVl8irBxP1mUmwdQOkzHBMsyFSbRS4s3bUmRG+Qnb584r4kgk4Vs0B BI7iCz29HHE5HhDbDhgT1s0C++cn7DZbKdHtLQ0wZWZrxMTMGJHgaWpmn88T54HT9L3q3GzqdlK0 y0qVi8ArY74zhwLZHYWKHmCAhRjBORGinLQSJCo3gaVKyVzHDLwp+45N+NbVQVmtYADnnh8298Zw 60iSOuPnTuGs3nDkJfNDM0o4pTzXGVjoUHOQo02vPTtjkmo79oMULvMOngNc0wCkmRTM2pDLFq3S wFRAAlwEpY1Bx+oIcCHAp4QRESOQ61UAFq2exbLsRbt3RSgCThlm/XCO4JcBYeGx4hWQEp7Ts5QX HiOGYUDf9xiGEcMg/U7GMarlh/Neco7ggxNL5HqJsFzAB6ly6p1H4xSkwxpomQ1O0h/FxTrXjlwG 0PXalhNqOidYdWBzH4lQFUYJ8gIurOWB9/ANsFCLVhM8bi1hGGSskmGl7uLyBDNNdJqhlQnkez7q e5hr1bF0zOVhRHe64HY8IfWDlE2358gLz/a/ETQqglBrmSofSoP1yDiDixK4T5g+Vzn3bYH/5jm1 gqB75NFskAKhu8+N+dZv1feaCfT8WcVT6md8zWLxCHy8CTaVP060r+p+9v3EMrbX3CKqTxZwYfIJ 5dQIBYlIQHIa4CndhZkloithxPHjC9r2hr7r8OWPpFdRsOq/+tCT/3ItjbdJnMAIectmYXEfpDGf 8MzYGeAoAsSRg3diwejaHkPXozvfcH454nQ4om970ZJWKyy2kvnhtmvwskEK2r9BZliKUlXM1RhN qWUwe7bJXra0vSkaLWtEuZBQdgUZg6+FxoNx18e9taIcTq0aaYwY+l7av18uuLwccT0d4aI0QNvt d3je7rFdrxCcxFQkTvBwKohSFiqgwoCTe9y1thZCNm+PDl8F7Mr4XUXE0+vVGyvrkvMKmrUgZzHP 2pueSgqzA+WCRQICDW1zjqiWwFzKYMUqxxKUiZJ+Xldn5mlw46eOyZgysNS6FZCARSSWoEgDpoDE GlQTbfdyBi5YrFSepXy4NUTjCFDwCKYJqKWGCRPnFmAxH5VMwOSWxQLqHIKuRUpJzKA5cM8gtYEf ASW2j8S6qClyWpEvWnVKaJdzjRwnBzRNg2azwJa3WYOymIdkXIiA3GeAICgxxysJkWTyymXaOQMJ QOJW2MZ6t5CF58wPK9BWGLBUGnUwJg2Qk7gtJjEZj5qqx17iwFymSQC0hPcORB2GoYy3fpZH/n7A BMT9M36fh9pcRTNNBJ8Y/a1Dd7ogtT1IG5QJLyvrY3NrV6keuowhj5MrMFGOYvaffRd4ld985xiL 2emVCnN3As1eFSvHG4L+NRDwCgB57flfs2o8Omfy6LXyUoGImpbq9x+CC1TgopJfmRGZAHXGYzUu I2nzP3B2n/a3Ht/86mvEYcQPvvwBnt+/w2K90n5GKk/JAkvvxzmdy/J5uEdbdgHzOyLv9HxWYi3k I2lxAR4cI/pbi7EdxP1xPuN0uaKNI6In4HmLxXYNWq+BZYPkPZLzVYkMgidJ7SPNxkj6U4IBp8aY HMRezed8gDKRVrND/NY++47lDAMUdszLQ+er6lzFGLMrQTRq9dc7KVWOBMShx+1yweV4wvlwRH9r EQDslysI1W+GAAAgAElEQVQ8rbZYLRfS+jwELe87guE1FiBONlipy2AgY/5glBljPV+WlQAjXqgw 9QYuXCYwI3iz8EwsN7a9qcxPDmCtGJD53Ut7c4m1cKy9M5IIMcdQ60yJQXGOtD+J082FrIGiBpo5 6l12k1krZHxKy68nw+RzbT1ZtZ8c8EkEYql5IHVmpAdMbZq9i0FR7dto0bONgRGTtnWPHq5ZwAdX QAUR4CmnlaZPMGHSRbCGSd45eJb+Ko4iEqSduwCLKq7GKjFqrIi42grTzMzPgIAr7yVoSijyUoA0 Oyk7z7L80r44GmtiWQWmATNrjRFH1V62hbXuOQaMUv5szs9o9iKLSdvP+gULzTG6FHcJ0I0j2hgx MIvlh4QeiR18ECuGcx7e+2ytiXGU6qmpooO8D8rDmCLwuqj9vKNWruqr2VusYNIxwENCd77idrqA xqRuXd2fKK43oTvKn1n4vTwuozSYsTl9PAbbCzT5G2X8s/feOh4raI/47/x6dh9b/fv7PYq9+JzP Hn3+6NxHFov5ePJnulHqq04+I/vhu6EUuJR0X+taMlDiney7rOZfPRcAO2VjpHIWwkdjBHxwcMGh v/b4OHyDse3R3Vo8f/FeXPTLBZiAIUXpR6J7//H81OvAUqGzRk2z4eskVJqTnuecE18fEcZ+xOUk Ra/a4w39uUU3jhgcgbYrLPYb0HYFv1oCiwajITedIZfUZwsuqavGGHSvsi6O8g/kXw/GWKM+syJ4 L8xChM+MsGav68yC+rcduTWu3ieEIIwziqWiO7c4fTzidHzB2PXwDKxDg6ftBrvNFsumQeO9Nmcr fnMxWXGFSi0pTv2hJAg0E7DJhAogyfAsAtnl562zPsqPsnMjFipi862tHmPM97fYlTw3zHCREIeI vu3Rjj1SP6KPI9IwYkhR15cyyPAkhbkWPoBCwHK9EvryAS6UwEgAmqYlOyUDfbb50SDSz00XsrXl 6boSGIm1GJrVVFB6nWumsnmFbnMWSEoI+gwOXKwKEVIboxGQy86DEpA8KQjT9c6ZDg8f+E5bFqAh LZc9J5jhSAS6uh+SKgsVDeVGTFQEI+X15cy0coG7fEuhL7O6GH803hE5SgYNSuE50oaIOcOJTDAY T9WSxbY9dXPX7q1Hh+0Ra8Bor52CGyKzRshniYE+RgwxYQQVxqvjML90CNqELwT0fa8/g7qA33qi 3/yYr/0DsQmz8DomjF2P6+mM4XrLblhU/UEyIzVrVnXdDCzYzkWxHH0CH7zmKvic9x6dMxX+s/Gi FuDy7neNd3krBuIty8OnnnX+XpZVk336GKoxc8lQoiK4J3OBQrsga66n/JoJBVwYsKWcsVb80Xqt JC4RZwrEKO87khTu88cj+q7D6XTCuy/e4/mDgAxvCRyVkvX2wRLQaZNojPYu0EQ3LlloP4RZdcOI /tbifJQo8+vlimFMSOQR9mss1xs02zWwapCCBwcJ1IpJQrgcV4xNdz87GW9+fEIOnFP5mp8HJkQh DNAYhIPTgDGfM0CsANajY/q+skm210Vjz+c7yv5m76WvxTgOuFwu6M43dC8XdMcbxqHHZrnCu/0O u80G66ZB8CJYCEBKY6kR4oUwOCHX0TBUV2uWzGVOJgCAy3ukXSalMdY0E8YmlXVuZZQ8GeRrRcjy GWyARd5JMWEYBsRhkF4wL2fEPmHoevTjgDiMiByRxoTICZ4oWzRYLUnkJYDVBS+xCk3AcrlEs2yw WCwQFg2WywYueKmZAC+lnmG80QICcWdZmB932kU1QqepmwTkIF8k0rgGzn1ZWCddwACy8CIALqo/ WN1NxLJfUoqltLJ3Wh/XqbUHuXMv10DlNSY2A76SmeO0b4tGNOiFLPYAYFV3oHupAM5oVUzBVT2U B2xEGRtnk4i8V8yzsp5m0aotMhmE2J7nyrdVoxQwqDI/GXCePUaJSSFks5FQ9iiBa7BraQotGDFJ 9dOkwEKISMsqc8rpwDKdcoWmaewdODcipTLvuUneb/EgKCB3hNgPuJ7PuJ0vSEOETzYHuv7Vbiia dtnuWVG2PT/RtF87OPNevMIrvt+jCO0JsM9CeMa/uYjyt4DEWy7vRxaYT13ru1hr6mslVQQmrpHq MCDhSCySWcBnxU42NBtDNHWThLwpN3YigCSRwmtgJxhiWXUEggdHRnu5oes7tF2Ltm3x9P4dNvsd wrLJvHs6N8ZTqn3OKEW07CRLaaPZxNoViQmpH3C5XHE6nHA6nXG7XTHGCPIBfr9Bs9nDLRuExRIu eMl1d8qUYpL6/RDmZqZSSYGhcl4lQLMbCYXRGLusu5lazn8wLc77SfBl0oY89SLWxEksQV5iWc6w RQMLOW/TNCYEcvDeYRwGnC9HHA4HnE9nxFuLZnTYLTfYv3+P3WaDhfdonNOIf+Rdku+slQfLPOha mOAgAwM1Ekaeo/xaJlXaYDtfwFWl+QN0v3H0Qg7uDkgVZl+0HLbvJCAOI27tDefjCefjEf21B1pZ 5xS1qCwLkuAEJI6ImhXiLC+bI5gTbqq5siNpX+89nHcIwaFZLrHdrtAsl1htN/BNRLPw6lNUl4qm RgpP5TwuS10ub2UUhvlBJAVqCMiCl9nB63gioprGLYOhAAxGzgaDSyWw16tGETV7inkUX38CaCHf SV4YQkKSDoeY0mgd42NCe86MWNcmBIvZAZKLSFGBDdfuBr2HIghLUyTns+VrfsyFCc1ibxjiInTw 5bkqa1hNg8IbK1SWT1CUXFktYLRXrRHbu7rnTfEDSAFg0r1WbsEsmUk5oNO5irILJyYSnmRpygyG Dx4LEkvGMAyIMWkgq2WG2f5TRv9g/r6vQ2pbkLbV7nB+OaK7XEApCrDI2qU9xYOAfC7US7WQQKGj VwV0Pv27j/KTQY+vHraYn/58Erz+xv0+9RnwNgCpP/8uwCL3DCnvwtbotWfKK8oWWGluEflcJZNa WgV5syu8ybPFeUkWFTsp4Bi0h0hkqJvNiasWI06HI263FsfjCU/v3+H53TPW+y38ohHlWG+cJXCl HACEIP0onI1cg9MIMSbA+1ywhpgwtgPa4xnXlxOu5wuu1w5dioiNA1YbLJ/2WOy2QAga/OERSRCV DRKgyifOZUPa4G3RqrWq8//zRCq3yFkeTrRf6VjqkAt1m+k1C0mNTyCH0rmuQBQmV6qpqbDlCJCm dzrIAsSuw+1yw+HjAcfDC7q2hXce+8UKX7x/h9WiwWqxRBO0iZpVZTS/GKjMBwCOBqgoqxPFrFaY gkyXtpNXQiOSrI+kmvKyCYCNnwTHshGDBvmUeVRpyFE0vBSRkLJ1RQSZMiQt106eMHYjrqcrLqcz zi8nXI9npGEAGPDstUIkgTlCWnQngDyYSnU5Z/hNGbTwuwSXAlKMAEVxnzgCXTucjxeQI6w2ayxX C+z2G2z2O6xWkpEASIwDGUhWU7ykacka5PkiOcua6BmRMGNi4ZFfXqpNOg/PAWkYVHstRZ6E1qKC 15CJNO81gdLyXiIQJzAPSGOUdFXv4HxACoTBkeybjCJruldQUAll2zc2DqeWF6ExQgqU1z7VnVfN x84MV+V5G5SujznDo1qIThirfTtL2wJ8DZ5PhFMRgMUmxJCaNbYPSZmY7AudTrAFbEGm3szCYrBJ mhrrEZMDKCCRR9e1GEZ1qxniYAWnihBTijnANUHS8sxfLdbARgDKMIKHlCvuWrGzmAX347n71MEV TVUrL6+SAFcfAR8TbpcbbocjUt/Bg4VPwTAOVdcjoHIn5tUzIKGYjvIEV3e1OS4i7m5c86DFWf5M fh+owP3d58YQ7PeDmSkbanLfR1P8OcDgcywTbwGIR2BAaFziiozP5OEBWiRv9o2sUOg1mQufJso1 fRgJdWq8km92UWZlJ2mgOWvQtGN4p7GBGo8h7hgHVotntuBB3qdEGFLEaTiju/S4Hq94eveEp3fP 2O638N4jkQWDG1/VWlOcEABGjOJ4kWAPbaCkAVD9OOJ6aTHeBnTnK64vJwyXFn03IjEhbFZo9htg twLtVuDlAlKyXpnfxPQsm6+Y+SeUcL8ZzfRoWpUxTd0dTov2eC3R7bxHjqTWoDrAgIXFGQCWfpvr GViwAplA1nh9FqEvi+SAMSEOI86nC67HE86nE8a2RwPCfv2E9WqD9XKBzWZpwfJQKDlDeDXrweQ5 CSIAiKwMNWYAaTZ/3lkhDYCCAo2g51OZY2ZlVpTdsUQQZsmi8QujoXxLM3NLZiwBQ8QwJAxth8vp itPLCdfzGUPbg4cozNUFXNMobpkQQF5+XCCQb4AAqW/vVAibqm8pUpyQRgalmKP0kzYdG2NCjCNu 5yt8e8PxdsP6dMZ2u8Z2t8VmIzVCIo9TxqdM1Jiqy0maxfJVb1SFIsVdyRogaaZ3ZlAi6dCZAOYI l8FSFUTFulZsVqoqOJI5561TYgEuASCWWgypAtqw52LCo7ofdmRIlLhKQ5V1d6S9T5QmiB1SFTxm accGXeZs9CFjvbtzhspTk2n+995SVL4ogOJ+RFR4A+taZouTzmq+lyAL54XBRQ2+ZHKg0GCMjGs/ YGTZ48xVQKa6fJNaJCyGpzy88QjJ/29IrD7EUk4emg5sbjNzFd3N2Wdp7lxAd14iTeFlwCVGYAJ3 A7rjCbG9waUkMUIZ1OXZK7KaDcJN+Yk9VwE1rMOdgQfLu/7EUQOU+8/KEF8bO799AkyMvhqj8cb0 1nR8H+fxitXgE2v26uf3LL48aw1abNdRFhW6XrULlPIgudqf5kotcCMzfHmt/UckSSKpQq2yTe8p pQJEAXRMQJTKwJ4FEXACurFDGiKGa4fu3GL44h12T3sst2t47zGmqLxUsANTQgihkWC0FEEMadsd GWlM6K4dzuczDi8ntNcbxm7A2A4iBIJHWK3Q7LcIT1vweoHBO9VKdXyvrMenNlcNQJiLhmsBZ+Z/ svLUPpcnLhqd+LCq6ynay1YQvRbra2Nctj89KYuNwvxjP+J6lP4nt/MZqR8BZmwWS+w2G2zXGywX KwTvwDwqHzKCYHwKIU+mS534dSBddkdAicJpPIWXEssk5hsBJwoiXHa4yTVLcFz1HkG01sy4xSct cwCxVqSE1A3oTlf0lxbnlwPaS4db24p5uQnAYoHBAaMnNLt3QPDwzRJh4eHDAq4RcMEeAn4cA+SF 8KVNF8AOCRFEHsxRO5BqWugYQWMC0oDUjeAYMYw9hnHE5XTGZhix6wes10usVwHOU06BzXMIaIE3 pxkeJAK2+txSrSbtpO0aukA+eLFeEUmdfk0BTSQ9aEwWQcFiLWUckQgjLlY0RBVMnJCS1GJwXtIp odpVYhbAogJnTjN1VdYcj2PgQX87ZfreOR27Al+W8Rq4MCOAkl2hPftN1Xw+pOVPC6C3jrwzK5dK 3qt6fZq9gwpHi0DNcA4ghzEy2nFEF0cMYIwEDCwxQEjyY7VOAM5y1Cyetp75CQloQoAnj+gj4hgx jmMu3f6bHvMZFKpVyykkLft8ueJwOCAOIwKEfh65kO+eZgb88pxheu7nBmL+Ux2vBmHqP4/Aw6Nr vHbOo/fvskBeuc5r39N3UYPi2oIyv/7keWCZPyyNGEl6kAi9KX+nZFqjvK7LsyT5g03LIkIEVKGS QHyXAOeCKPIZ4wq9D63Ez/Vth/Z6xf79M54/vMN6t9F4DK/pq2IhCUM/wgEI3gMpoR96dG2P2/mK 8+mM6/mKru0Qo2iWEYwUHFb7DRb7PfxmBSwaadxkTOoTe+tTwS9TJknZRE9qZjJfuIdaLGb3q3t+ 6KpMdk1iq1wmwTEWrCfXlDXxYHCM6K43tJcb2usF18MJt+sV66bBdrXCbr3BarXCwgd4R/oDpDS9 /9uM+NH8WJzI9BpmdnJOKmlasCa8anLWEA5SLL6UFCtGbienZaFhXLNuu+udl7YnAyMOHYZLh9vp jOvHI26XFrHrERMQHUDrJWi7RrPfIOw2wGoBXi4A7+FDAAUPsoqfJNp/VEFJGnMxX3urdwIoBklA SAyf1Fw4RFBM4LHXINIefUw4gXHtO+wDsIRH03h45yVFNxW878xyNVGwVHswK1nWGORZJjEtatGh JiA5QkpSfTVx0lTQKsuhWvM6tsleZisJR6SoAvOW4IIEtVLwSI4QIbVg2JgDKuxQ/RRApPdEdU4G PPaj/yoDylk3XMyzxqQtmwiwALOaXqeLWNIDf72Dsur22vxVA7XParARzUTrweQxEqGPI7phwMiM kRMGZgxJ2toTW12IavKMB6HSJvW22RIG0noYGvtCQEyaDmxgbSZ0PosHqCLF5U/hg4AWzXKI44DL 4YDb+QJngEjPK+Mo95yywKK4let/xmH08anTPvd6/2hHtau/A0B6FDw9f39u+ag/n1jQ8GkwYp8/ BBJ0/+zizCjXSCy8PKllQ3iWAAtxk0OIVwsDWq0C6dDKuQaGCFpN+yfhRVIeQSwYJO3Roa2FMHDC KQ643W44HY8Sj/H+HTbbDVzjs7s/eBck77WP6G4tLpczji9HtJcb+ssNaWQty0zS5yME+M0a/nkH 2m0xNh7RiFeDFiPHT63nm4cFGOVeHA6ljKlaLDLSppkJl7U2ulK4is5K6y/fsfoUYBEG3jlJIUwJ 463F9XzC8eMLzocT4jBguVjgw36Pp+0Oy0ZiKryVT9dg0aTtz+0BjUHXlpRHPQrqI3vMawbuvLoo PHzwGZ1KMBuVsUHM52DVyskB2X9vPkuoxpok2NGVYB9ihwUChnbA7XzD6XDA+eWMy/WCvuvRMwPB YfG8x/r9E5ZPT3DrJWgZQKFB8g4DEeDlGVnpxgRXAqSvCD1gtMrEvbkRzLRMAPkSNc2NBR2v5Ess tSQ61T5jHLBxHksfpIQ6WMyDJkjIwaWUY2gy+FIEIYHVlQZauQsy32aJ5fDBwXEAe9FcE6LEU+Qh TRkCoAGcGaxktULcWFE0CMQEigwsWDNoJC5grBknVV+3S7HKRpq9b691PKTPARsX1amlVBgQaiCq n3OVMTNZP57sr8fHp8QOTbKVMtCrrv9IXtT8QOZBrHfkJaWZImMYE/oxYkhS2TSyWOksuNHsJPlf 9YtlC6TSp9W6sH+lIJlHcASXPGgUl16M8Q5gfF5WgY1X6cxgIENcXonRni84H49AHKXoYKp7H1VW W1JBx0C9KsxF4SjcA5Wb6IHgfGXp5uN5NY36je9Mvv8bWn5YbvCdXRyPnum1uIxHsRi18iCs6rF1 ApnSXp8Hrj43wGip2g/P098WZAnWtHxtxyHZY7ZvjbZF9rEqW3Ci/EkfGtIieQ6ckmRcmjUVUgeo H1uM/Yih7dBdbnh+X1wl1AQEjIzb7Ybjxxdcjmd0bYfu1kp6ypgQ4OHh0KeE5B3cYoHl+2dgu0K/ cCI4wPBM8FyI9lPWizw5s41XzHiUByRVqa2a5Dx1dHpQyUmT6xclpLJqkgZYcg4AJWaM3YCuG9Ad rrgcj7ieBFR4AJv1RjqU7nZY+GDhkmAVCjKE4i+8IxoTkqCHzNGORMq8lMGbVuS9aOHWHMpQJlFJ DRJ5QMWNxJaUZjdPsG6l9jlcYUAUGRhHnL49oz9LlPDhdETb9UjOIex32OzWWL1/BtYr+O0SvFiC swavIEIrIZKa8LM1os7OUEGIykLAAFyiXC3R6trb93M1y+BlU5DT8JgGiRI4AZQS0PcgIkRfaj84 EAIJjToFGAF2L4mgzsG0zuJkMofOBZ5qus4ZG042ndfXPEYQSm+KuaQVAa0FpWDGTs4CbKEpYWNk cEzghQctpOaHlX6vL1nSkCWIzD43sEkVCHE656yAuohJGIrIlrO8E2f3yxlYmGly9g2Cum/mhE75 mo8P+dyys+bfYP3n0RXyFmBozJiH9SiJI6PvO7S3G8a+l3Vx5aKciwLKmhNsbiivlQEMZisiZ/Vk 8iwLf3IEkEOKEjdVAwzM5uuto8S/VD+J1UXb43I4oLvc4EHwxCC1xj60luAeWBjAzPM6WeHZueWP z7JcfM7YfpPPqVJMPscC/t2vX+burayQtz4TPj8FFnPw8fr95wBaKOEBK7kDFwUHl/Bb6xWiAkjk ASn4TtUXYZmsuuG9lAt3DHUXe1AiOMdgzeRDTGjPNwxtj/56Q3u+YvfhGeunPcI//PyXOH77LWLX IfUR4zACpLUHFOGMKWEkhmsWWD49Iex36BqJamdIsKMNgpmlQMejSWMbBmdOoTJZZaXGADjSqppO gwRNeD9GimRCyia30rjugIV+IKZQES5p1KY/xxP6yxX94Yyx7+EAPG+32O/2WC1XWvhKEJ14Jij7 mOTWwhQNbc4PzpzxbeRFToI0zVrj8uugrhzVjLNGV8iDiZBUJc3+Vy5gT8SeEryXzAIeI8a2x3C5 YbyNePn5R3TdiHboMQRCeNqheX5C8+EJ4WkDXi0Rg0PyhFFBBDkCJ0KKUSqtVqzLgmyt6NZ830ya rBHU4qJWGRgQ1I9JATghl4kHW70L1fGWK/SQ6osuyXnBAQ0TAhNcigi6wQJbkKUFusW7JcoCjlHm DkA0+iIBQk47oSLHjEjUNGdNVxlMMvotWnMtxLwxnwhp68VaKyJpLIYnretC+fm4IgWmUicjT3YF MMiAhVocE5XCazKcEvRX5p3zWOvFo/z0mLzOGj8Aszh8iu7z5xnMUP3W69+aKCfCs4gcUmS0txbH W4evjid827YYvAc3ARRcblnPVs7cBABTpQGWeaTZG04ayU9PgsWCCS1KyXsNEk1WYv0Rd9ArVMRn sFPAr8R+uTGiPZ1x/nhE6gc0gAJkgllgCQY2WX8miHhmWfh8sPA5Z77G+77v423gUCw3v+bF9UU1 kgw8azCmn2erTs31ppaNifsD9/RcnyPijIqirl/I4GKmkNW/gVJsy6zbklmQpOYNqVywirVUyRI2 mSmKGyzTU3mt9Twpz5zURS8p3pfDGd2tx+l0xvb9M8Lf/NVfYx0W8MygUZ6BtAATa5R+JCD5gOV2 jeXTDqNWFJSHklAjeT5WTfpxYZmyUPUCVsACIrAbdX/UFSVtAWpgkZlKtUCGzLjihBV/BSACxzFJ TMWtw/V0xflwxPV0xtj1WHuP542Ais16jcbKZes1iGzRYwEWpqmn14m+xrWvH2LGhZdS4qVrrRCK eVczaRvBM2vPCi6Nz5QhmiJut2aIP44SI43AcO1x+XjA6esDbucb4gAMALBaYf3hGesv38E97RDX AaMnjERAI3EfltFhXgR2pYBStrBkS4RaYmbaUHmtSSmUYH0q5JnrmBFhuQIs5NqsRR0CEdh5jE4A n9wzIUACjUcwAjMatfZIsSlSs7JYrxyzrm+l+VXgx6lWQBANPqcEs5jHCR6uCeAUESFlo5lSAbls Nf2FjmxRnP3Nop2CLD0S0mK5V7DiHcKykbkXOZgBBQBY/f+c0VzJw8k+sanXNSJFKE73UE2hZc+p JjSxdFTnKoOtTe66rA+vd3/oc1lJzQff/ZyDyGMcEs6nCz6+HPHt6YJvDiechh60XmPx/ASvcUHk hWnGapKSzmHRBaYgqaKMKl7OeBPDiqd5s7SyQ6IkQCS9Nf56Jip+w1B3SALFiNvpUupaJAbUbG2u WVKJVAvYCe+dzO7Dm9+/lQXoG8/8mVaZ3/yoBdzr/PbXyfyYXp4xmcQJD599zvfnv3ovvn/uaexF BU4qvsHV/po/6nwWuPpJ2pqbwdlyKfWzhJNZkTtOKVdPZiA3qHQQvuqhgNfunwB4qDIp9Z/6sUff STyGv3399f/o2OF3/4sfI/Xqg48MsENkloZiTQCtl9h88R60WUn5budgZm5zE3C0MlAFBFhJZUCY ca1lyFRJAGEIQYLviBCcq4RLgWz1BrGSwiYECvAgRWUWZFUYgwREEVxi9JcrTt98xMtXX+P0zUeM 1xsW5PC0WuPL9x/wvN1JamMTVBjYdk85oy7rY6ouyOIXwcTGHTIIIACSNqr4WgqhOAmacT7Ah4AQ GonYJakzUsZhwX8WsGauByrz6wgjR8V8SqhWiZBZglXZI956dIcLLh9PePn6gJdvDrhcOnTMiE87 0A/eYfXjL7H60Rfw7/aIqwZDcIhOalXk0uFJU1mr/3gSrg5g8rfCY0iGBVf/ERjsVZMmY47Vwtd/ 67rmgk82zzCNnvRUB0tLZgUVEVJHYgRhAGMgwuAhXXm9rrEdFbDLAZDkKisc4GydMsjW7qUTa4Zp BuZKoYo0ubqWEFJd28VSHqFmcUrI71VTIYzEAAfNpy2LQKU/ylvLaNXWIX/nwWFLKK/pEXK4/05B iRXtvv6TkPL3LFDSrH4Mrbxr6eJGddoQbhwTbpcW3379Eb/8h1/h47cvuJ4uaG8thm5AGiJSH5H6 MWfHCQRQYOcg1Up1Yg281VURJ8PMa0f5Y0lxxWQMpizNCyjdWWEnSpO48TwDPjFCBLrTBS+/+hXG 8xU+JQ1yLlZKwJQ1zU5HVbdFx3BXwunRmj1cw9fBw901/tFxxhs3+AR2+BS4qD/9nPiP186Zyzpg utcffT8rsTTdZ7KWdPfduQWDZr/rWcosOCukRW5aTBxXJ3K+gK19oUvbd7meEk8/QWKE//sv/xp/ 9Lt/AAevVRTlBv0wIKyWGNKIEQmbzQq0WiBqaWlDNQRgjD14TFg0DTTeI/efyCCjsj7kSdSf4EOu 2mmll21m5utGsBLNJXgyb17S6p4pTb7oQQAz0jCi73u0pwsuhxPa4xkYIpahwXqzxW61wWq5lHGo 9QRc6RBkvT8q075Tj3kViu8qri2EUwBWXSgMFcPJpabJQFthMvVYjDiyPz/fQxYUidEEJ5UeNcWR vMTNcGSkLqK7nXE9XXB8OeF8PuPWD0hEWO33WD9vkb54Bm+WcJsVuAkYyEvFwsw9kSVZFrn6HgMY 1dRWO0DIFlSF2iPmxQq0kHJ7JZni2RcY0HgFuTJrARrbDVJWmiYCNZKmn2p8zaAuFE+sQECsh4EJ a6wc7kcAACAASURBVPZYJCcZBKRWhQyItPGXEGumPXMQJDMVEIEc4BFAVNUWINJqmcXaUwam4FG5 S43PnAqZxACnESlqllCjLeM9AQbIK/NltsBM1Fh7VlhweaHNai1nizddr/qx7z9+9XibWReLXIK5 FwvN2xkSM2CgQjSrcZDy+6fTGafjBZfjFe2tk+Ja7NBEII2EcRwxdCfEcIPbdAj7Dfx6Bd94wHOu bJqnoHqGuqNvYd6FzuS9x1Vf7aiVrRxQPjk4V/4095WtPQ8jrscjhssViFF6MjHnFGO5l81XDXoy EkJBQPrOQ83/lZiAV5bu+wYWnwq6LBa/f/zjsywd3/m7c0Q+PWoAn89XbaEG5/U8s66ZcbzERb5i 8gOhB2uHoUpTCfIkMDuptEuklmjhX8ySqQRQVQisKALizXdaQRYI/8Of//f4oz/854hjRIrib05J tOFffv01Wsf40e//Lj78+IeITUCfYkbxTVCNMAEcHHwI6PsOHFMWmraBBNhQ3lxmpaibamXLwxuT bgOphQ+Rlz3DxVxtWiKroBq6AaeXFxxfXtCfrxi7XiwV251YKZaaUupFkMJNYwTyOlIlMrOcrdLG uFgSDNHlYkygUrKVCM4Fqcyolhrn1JfOs2yYOcDS59LigfnuBCEcP0qwoNzLwUdCGgbcTje0pxtO 3x5wvbS43FqMjkCbNZrnHcL7d/DPW+DdBoNj9CAMDHitQAiinMfPVLJUbDNYFoQAyalrrHjKimVn ziRykk9G7AZEpzRABtBIvmT9IKaRL+WLYhVx2V1nG5MZiGRBwnLfBtD6IAxP8r64JyQThXSN6+ci BZVJJVP2m9sJVgxWgZO08ZbCdcwlcNcES1Y5ucytrK1YdCTFLJVW4MmBGg+E0pE1VWuTZ74y69bu FLNyGHP6LfLuu0MVoGmVVLNK6iJLjRsxxcYYcbu1OJ2O+PjxBcfDEbdbjziwGhml1bQbE5pYgcSh k943txvCdo2wW8OvF7m08YiqJwqoMFprMmiMfOICIl0zQpnp+8MaKGZgmq2uyPzUBykNkF0iDHRt i/Z0RuoHuJhAUSvaZIEDXXDKMRe1Tmsg3brVTuZ89mqyJjNw8kgT/9zjc4X1byLU/792PBrLPNtk 4vpHLdvo7juF7qZKlwFyaxZoRDqp3qm8wbNliRSLM9gh97UiQnIOXiuD1rntTAnOAYkdPKTvVy0n jfbDf/fnf473+yfEcQSnBE8SNHi9XPC//Lt/h7/99pf46b/4I/zrP/9v8Ed/8id4/vAeHox+GIBo AZwBiRnDMIglIjRIKWXrRY4b0BmzqGrv/GSDAWYWngmdilEyS/fR3OWTgRTFxA6dVGL1h6eI26XF 4XzG9XjG9XzEcGtBkbFdrfF+94Sn/Q6b5VKZkPpUrWV0tagPicZ+agIgFZ+mAVH9fbmo89bzw5f+ FWaCzYKrYlkzq4/MeTGx5jodziFQAI8DFiSpoLFPaK9XnI4nHF5OuFxuGIeIgQFsN1h/eMLq/XvQ 0xppuUC/8OCFR9RARMtm9XA5ij832KqUtBJ0rDp8DQjugDrnubp7j1n7hNhbNXR7JPiSoQ3YjRNZ VEQ5nJGfghGp0JjyQ0iMggCTVjekJ4aHVGP0rG4OVtDGol9nEcgFNNVBjXJL2bjeaaI4CDE5pDGC OYJ1Qh05kOPs2iBonY88bhEyRARKwJgzUoTYKHnAJ+2QWkApgzPjMdCVgQUyjpFnxeyN7+H4LgKI lAFK+rk0BUzmElJrRSAPjgntrcPpdMbx5YDT6YTr5Sp8ibUVPQM8RsTI4MigRNlvDJbCbP1wRdt2 CLcWi/0Wi90GQS2XySyhiuLF9ywKU8ruPZkqA6dzunt0zLsTixW2rKVlsLFaoBwzYj+gPV/QX6/A OGpFzpStXyYQAMDlMnq2APoPPXi6h3vzdfDw6wKL7yMe47cX03F/37eAzucAoQmAQJFln/U9BatZ TvJk0VH+hWmu+T5TEGlWXrOASldky8KEZpUQADiJ1QMgIRAaaeWdXidCShioguOrTuFmDQnPuycQ pMsnMTCmHs55nG8X/NV/+hv8P3//M/zlL/4e/9v/9R/w0z/8A/zxn/4J/uy//q/wkx//BBwlkt1p HQVH0l1xHFOeGGt1Po4jYkwIISD4oDET9wF+2bdXm/WqgxQ8kGoxbIyHNEqbpZJY23a4XS64ns+4 nE7otR3xwgfsNmu8f37G824nWkRigGLRdBUQmMCk2f2zkH/ISKRqWgEmSgfkcvyED0EXwCP3N6kw lVSWtPGX+9RBP7UPlHSenGpN3i2R+hHd5YbzyxHHlyPOlwu6GNE5gn/awu+3WLx7RvO0B1YNBk2h 884DIyNYBQgVdFGtFmI+S1nw1RpwcjIr5al0PLNJzKtdzXd+i4GGfQERZNUrZdGTbrZkqjYY1jbc sILFD9i9xI9v1y9CgfThLUiUWMzxnXYODCQ1N0YAC+cQWDv9cYKvg2TzyktQp2SREICEEhNT0IKn BhQd2GlVR2gWgSBb8CwgmgmT/A3bAy4HBwoQ5Jiky+yCxF1nFT713hno2poZ+Kl+ZwX6M5TGYpF7 45zJ3tZZekNIkJqUZC1lkh1T7hXEzGhPV1xOF7x8POBwOKC73RAHVXSsg61dLSVxBya5Zkwpu66C 7qF+7DB0HeL1huG0wmK/g9+ssFg0YOek2RkM9Fk8BWWgRsorLF3ZxlorA/LeVGEojfE4W3nlB7l7 rvG52+mCy+GIse2l/Dc0W4rKtbPOeze95TkeggPmfE4WLhWvKXECNLnW5xyPzvsu330clAm8RXmV zP3sZ5pfYf71R2v4OUf9vTwenn4+mePamoGC8+vvz0HD/Hu1LJmMhTBROOS3AOgCH4TlOnZa/0iL TFYghpXWWIpiACr/vXPCqNTPHAiEtu0kkNJ7QDfgbr/HX/zFX+AH//k/4m9+8ff4u7/7OX72s5/h 7/72Z/jyRz/EH/7hH6JrW6QYMQwDAGCxWEifEt04tsnF2iCRqCEELJpmYgqcCk1MJx/3dGJ55lJk TFJhUkoYhwGxG3B7ueD47UdcjifEcYRLET4xlmGB3XKNL969w267RdM0GMcBMUU4L2XErZ20aXkm uB8fOuF8z4yNICwWRKwVEvhn/i8LACx1KurBPzalAShzm0RLXYQAsJiIh37AeBtxO1zx8u23Uqq7 G4BFgN9vsdpv0fzgGbxbw+03iE3AmBKipmcunQOGUYAiSaGh0RUjr/i4S7wAMaTWSU24qXwOQN0z hTV5FfbWNKzeb5QkRbAYRSyoLoFJ8lyYqFgi8rzNfmfwUX7fr2IV18KG7IHoPEYkDEmyUqK6Ihrd tJLGmnLFRLs2aZxOSpRvm+koD1KAjPNeC6M5JLLS0dZpFfnfBEziYe3CzMW7T1EECkeW/iHQ3i5e Gq6lXOG2CiK171braDLms2vUVNd5+PmvqWkajYmJVgpUgRl92+N6ueDbrz7icjrjer6ga3vtm6A+ ZAtIh3SBlVLEQg+psuCAAUKCZ6ABg4aE1F/R31qM1xaL/Q5uv0OzXuq+pVzds4oEQQa3bABwysBe E6615eL+PEmP91BeFyMupxOuJ2kO2KgwyNaSmUYMPKL1CmDcPZOC9HzalOOV2K/PBxbft5Xhu13v +733o+d4y81hn9fPXIMIA8lvuUUKz1Iaq65p4K98FzlOB0BxvcsFpJIn9BxnVlxSnicbPxktOqha Cc0+lMw9JpGxxkcEhaoi4FyecUmDJzAcAscoKNlLJ0vWB/Uh4E//9E/xz//kj9F5ws9++XP8H//h /0R0wI9/8hPElNANA4KmcwHA9XrFerMGqU/xdruh73ssmwaNugKapoH3PheXqRdDfjgLW9syqmxV MybvA6KlpDHidrvhcrmgO1/RfntCagcgRnhmuAg0PuD9ZocP795hs1khuCBaETmozRsjtLubc5hs sAnSFCsEYJUc7+hP5t3Gk4M1PUC+0jBKzEImRtHJ8/0yE4Kle+YZyeYxTgljHDCOI9rbDf11wOlX Z7S3HpfuhsF5uA8bhOctmnd7+N0afrOSzAgnmp2HZOwQA4mjxH7AYoEZQbkOK1CyxmfMMm+Eov2K Jakq5GUCnJErzFnJa7Bq1zYmQFvPV8ibCjHIGVUJtXyS6oxS7EI6ASZISmYl4MtDkgYy1l5pfQ4i jE6u6SjBJxFOzFqzwm6rm1pzXHKtDGkC5ybPNSEkRg62co6kwidFkPOI44iUIhy40jDK78m4CbCC aU4kZ/5o6KPMq2dwkMBP8gSX62Oo1am6+IxVvg0aqH6y+eu3jznTfXgOJD0YxIhjRNf3Umr44xGH lwPOH0+IQ5Sy8OxACdKeQBlsQxLbJK6GhKiC36rDEpE0nRujgEXSYFnIddr+gtiNiG2PxXaNsFnB r5cIwSNZDQy1ZBR9rszEXEF6NP5HQsrcnKTXcGpNa28trqcLhrbX+Ce1aGQBpAGYyjtzlU6UoOB8 XcZEeOXYpcnxSkAn8ycoI3/7VXXsu8ZS/FO5Qj51fJdxTIBFRRyvAgyUJXnNsnF33ZlFMNXPRwVw WJq7dYzOgZ9Att7C5E22LItFViyoEuiZRMghW7qUgycNvGPHCFHjF5gThiHqTdV87z2+/PAFfvDP foo/xn+Jf/3n/y2ufYsf/fCHuLU3+BByVOnlfMa//Z/+DQDgz/7sz/D7v//7eHr3lKvfmemvCSEX lsEchdkjEudNMFm+vKmEscZxRHuV4lfn0wnt9YrUjfAj4KKc60FoQoPdeo13z894ftoDJD0AUmSQ 12IiZur3vmy4h3QtAg72bKrp1c8pZVJLoKogP1s++aYF/U3Qbn4x13xqdVKjdknS8cZhxPV6xfF4 xul8Qn8d0V5GwDVw2y1W73YI73fAdgW3boBFwMBKYAz4CcCD9McgwAIqHEmhKYCkoVmK+UFt3PnJ NHLI+rkWbdnYXMVcqYzFLmbujKgUa2BTvlgqCpRp4sl0MUQz91ENxFYxlOuqlbJ+xptZivPnuhAR VQAUyX0thyGxzEGC1H5Zkod3uiEtzQAM59Ryg8rFQfZPyuvOKol8CDkOJ8WIYeyAuoT+DGjkObep 4RLvAUgMErgAUCSWrr4orhIDcEUYkg44zgjagPCUJm2+7jcJZzcUV2vz1lGfQ1CXD6S65eV4xsu3 Eqh5vdzQ31rwaEhAaY1JHlvnXwrbabM3daUNKUmpb83cSjFJFWJYOBsDyqcWRBjiDde2w+14wvJp i837Z/jNRujLuWwNyqNWoWEuW1vpWqOr05fLB1zRGgCnPZM8wP0IHgbcjid0lwvS0MOzXJUngp4q gV6oPO9L5WVU3fOto4DnOeT89MHGo1/57P/Px1uA41PgwV5bzOCr31XePV1DpT2qgUJ1fjkFRhvZ LVJZ0aSbsCiWxhuyQyRpcUyuSFevk8wfnVj4NTukZHIDgnQSWCKrAcSRgcUCYb0ELQIW3uGH6x+p pqnMg/RBOeJv/vIv8T//23+DOCb87//rv8dPfu/38Kf/6l/hX/zLf4l/9nu/CxDgmiB+ziTpUxwj fNOoQBFGEDmBOEn8hE2RbgxPEkE99gNulxaX4wmX0wXDtUXsxFLholgWvA9IwwiAsd1t8e7dE9ab Fdgp46GkmkcqvqYqVgKTTVL0E4aaKlNp9MWQDqTkXXaBCDFYmoDxa6qWFVlwTojJrDeVa4Et/Udd KZ4d+muL/nxDd73hfDrhdL6iHQbE4EHv/1/u3nRJkiRJD/vUzNzjyMirqququ6unZ8FZ7FzADihL CgkIwUchRUABAZIvwSehkD8oQuFDUBZ/eAlA7i52F8PdmZ2ZvqavyjsOP8yUP1TVzDwiMrt6Zg8h vSQrMyI83M3N1FQ/vVfwJyeYnZ2iPT0BLRpEr03DiHIdCpdR6pSYExgW62ADnLBSovxras6rDMaq mRNRaT+u/+oq4OWaxnRrl0ih5GkMdRlXWZ1i02CrwZHRH+8JQjUNK1cvzyF06bKFSAYlvVGkC+rI jIEZDQMjERowAjkEJy4bypplAokTJ9ONxY0UKwwUVAmteN/AO8kaSjxiHKPENVk5eDJQorpzFaBX CltJhoRJtpik7wqPBPYQ15/XYGrvpOhYShiTjNT5oDioBBjKNFUaFsziw7pu5d5mRLHA5qLZ6LOa QmH0wJr94RxSAmKfwN2IzWaHu9s73NzcYH2/Rt8PoqQk43oaW8EJFqDJBiiYETlK8z0U5QbMkgmn XWwNsCeurAnMEvQZATdGxGFE1w+Imx3as1OE1RLNcoEUpKdSAqT0vNKLxT45BTZAUpOzU8Rhgamc 50CsKkoHTsEsJ3hm9Os1Njc3SLsdXIwSdMeE5ADW6xCEqVvGVsYpKHNte+ioeM/ZbfkN3V48eYeO fzufwdW9jx3fNl7hqYO5uPiOjORJAFWrc4+PqMoABIQvw+RzZswH6Nnom1Uhs5nN68DGIwsPzUrC BIAeAxllfsm6T2bLk9JyStlCLNfcj5kRijFYbVVqiRyYKVcd9qSFCEktzUzgpNSmNJrhrH3mCE7d DqKUasEYGaN8Sczj4tsOsxZwDgMShjhq3r6aiZHgvdTRf3Z5iT/4g/8Af/HTv8Bf/ewX+OijT/Fn f/TH+L0ffB//+T//L/DBdz5A2zSI4yD+n2EQsBBH3VRlmZ3zygiS5ng7eBDGrsfmfo3Nwwa79Rbb hw3GXQ8akjS7YtFKDKgQCE3TYLlcYrGcwzVS/yGBc6aDtdy2Zi1lsUTLOSQ5aGCjokmSqHantdZF qGrNjsyElTgMLYIzsdYCk5SAInMugU5JUKSDLD5HYH1/h839BuvbB2zWa2y6XmICFidozlfwL5+B Fy3CbAY0AdGhVCDMFG2xHhX3z0ROeUAWQMkVAU8J/vC6ecuSmeGTAgwNfMvnTcFVTrnMRKv3cGUW 6/1mpuCy7fUctRJV6kEO5JQzY1nZPVOluBnkqIVkdEBkaVE8xIiRNNCTCS0ILTktM066wfcFbwEZ 2a+kYxTMoWN2Dp5Cfh2JQGMqGmFtpaAqA0Sf2xiU03Pl+hIkSFHiL4Y4oI/iOgnzhfyEmVhtkkYm sDEJr6mvpkhII6QseIzJAiXbChlnFCeW7S0WgOO0R0BKYkEYhgG7bYfdeovuZoPd/Rbr+zW22y3i uJ9CbECA1DCTJuAisezyyFyl6yZ1ETPMbGMaGvS3gQunANc5qY46xAFDL/FcYbPF4uICfjlDaFtw 8NIEDeq3dkAaI3KNehwRuBqgYZ+KTa4IM9aqmxgH9Os1xvUGbhjFxatBOGJo4gIimPP+MRFSHxP/ fx5GfU4lcvkQyD8FC4rw+ts6HgcWAFAsb48ADK7+OLiQKHCls6/xaqNjpWwj8Ecun5UZegTAGN+p h6Bgs5SGnJxegEj9YYWAsjJQ8epDa0oBGHmsNbWwyURRcl0+g5CgtQKsBgYUlzLUBS0eCcfSGykk FZSZX9mNSOIFQtvAac61+ASLP4ZZfJRIwIff/S7++b/4L/HRLz/Gz3/2V/jTP/sz/Oxnf4kuRpxe ngOOsN1tMW9naMihbWeaeufQjxGcIki7rqUY4UAIzoPGhGHbo+t6bO42uL2+wW7TIY0j0hhBkUF1 TqSytnEYEZzDcrHAanWCtm1BZNi9YHjishD7loUDesgLltTtIdqf/bZFrOs0JDr8vlMtxvxkOfZC I+TZaUO4yPARaEBwI2Nz94D13Rr36zXWXYf7vkMPgFZLzM9OsDw7gztZwGmJdiItc60AUsBQBZUf OR7z8R37vH7PZnJioq+EYQ5hzfOTJ2b6evr2ZI32RgJonIP81vXZG3Mem/knrcETuBJU1fmV3Mzj YCvsJOs3MmfBluAQScqKRyKQWtgsasUasQHTWAcGctqsFQJjEq2E2cFRI82DXJTCW9pt0ywYpVe6 VfiodSW5A+lzOoiG7kCgIWLsdth0HSJfg5oWYdaiaedwIaCdNeqmER7gCLlqJihoV1C9C2uAmAok UgFvtJZrtyoQQErgUQKIBVDs0G232O06rNeiNIybHqlLiGOaBKqZixUwoGoxNRVjNXDBYp3gmKr3 9XsWn5Kzn3QthbvDLGxsWhgAFxNi3GDc7hC3PZrTJdpTtWS0QeK0dK4TWXySdInmVBbdxHbej7pi 1lCPIoAxATGiX2+wuX1A3HWgmKRjs86xCa39H7FcGLBFnqvD42ke8P+V4zifom9+vIz594HK4/aZ /XkUEPE0kHmKjz4eT1GsyU+5WSqxXUb/BOoSPsaPn1shDHFOU1YehPS4PkUy0jJ4sTg6aL0sRiim T87BdARR33zwaGYtXPCgfqyKYiXV1CU+wTmPOAw4PTvF7//k9/HDH/4Y/9E/+cf4+NefAt5hcXKC bhwQiMQUy4Rff/EF/vX//n/iww9/B7/3/e9jdbpCH0cZHAhj32GMjO39BrdfX6F/2CJ2A+IwCnNg zg17iKeQwAEY44AQ5lidLLBczMUcnMRNItouV/P5tpjbgjRJXSBe5oYot601xGoi3OWFke8XplIT K5WFY2HEwpgYY9eje9iiv99hc3OH9f0aO07om4B0MkN7doqZdqFzTUAEI/oSvyBNbGQDWSDebxpU 9RjQsN+k/kJL8bSZFQWayhv5wm87AIV9dGSl2OmDVaXgj1yiHqeh/JKyWJ7TfJq1RUAgTMWsndPa CxbaJ9aygZ1kHjiHhoJa08Q/kUt4o7Rgt+sn1UKT3kysD9Lx1bMX2ooSpMiIiEpf4jYzYF2EK5Fa ztSyUeKXGEiM1gW4+QKBPO4fHnB7/QbbvoPzDdr5LO95Fxo0jZSkJ+/gg0cIXgK2c9ArlYJd9qwA oNaSISakGBGjuBhSjNhudhiHHl03YLfZout2GAdxA8UhiltKws5l1AYONFXTjgIiyjk1wIhV7Qhm BifNGMngAihWi8JDcsxNSjleKIDgolQFHm7v0e926Lcd2u5UMkvmM4TgEF0pYGbAhSpDVVZvnOwT ouJGcQlCU5ER+4jN7T02d3dIwwiXeFr5txIsmRb0xzbd3wZ8eJs4imP85kkh+Mg19/nQxBrDJXZg wlePXmj/jW/mhUfHpLy1WLsfu/7eHPDx9zPYqEZ07Hu1pageVs4+4uNzfmxM9VBN7xTjDOcsECbZ K2R0RQSuOvCy7kEbI5GUpwh50lgRtmpR7DxC26JtW/jgQaMDUgSR+GuHKIFfDIbzBN80iP0AFwih bfDyvXfxzutXObAKEAHcDSNa5/Fv/vX/hf/+v/sfcHF+ge9973v4gz/4A/zwRz/Cq1cv4UPA9lba fq9v77C9fQD3UpHOEaFR/1AOPNFJKiWPWWpaNAHz+RzeS5dKmXA9bzLvCjQmMRDlBPOfWQyBb7RO hRaqAlHWgglasQwmzOVaYkbSSHOmfE1Bh67UgkiS3RK7iH67w+Z2jYerG2zv14jdgJEcxlmD8OwC 85cXoPMT8KJFahqMkTHGESUxiFRYqQmL0xNoeUp09e/6/f3vToR2Tai6NmUKivUCfGT3AdWov4lh mTDTK+6t1zdHEYo2Tp6y0DpuieEMCvf5j3SfdXkzRpLbDixm6kgikDx5hCTxGF616QwH7LImdNTc nWnBwBp5zeCBdJ4dY9aGHUODwEjBpPjfDd8dBAyzWBpmvsHCNViQR8sON7e3eFhvcH97B/ZSf4W8 l+yuppEYjcYjeAcXGq1QXyrLyjpwRTtSvGocR4zDiH7oMfaS1TTsBqQUpdDeqNlqOkIPlLgKthgK 5LYE5b3ym+0crgI5mSW1184F5wBfWzNWzpnjSqA8oKZp7WhLgDTGAwEJGLY9dsOIbrfDbLPF/PwM s9USYd5gIMIoDFVjLCorRcYHCjMUyfgkdUtIf/pdj+3tLYb1Bj6Kq4ZY+BpVaNcpvZhVkPDNoOIJ RfvJ4yme8U3f2Rd2bwNKjl3n7c57+/HJdw6tF9PfT9+PlNkbFRH2QU59LmU6PpopwnvxbZPPs4qA 8uo4kJjwaCArvcaThe8cATumHNgmIXMdcmaHnIPv9+6X+yVxsVzUQgJQ/7x3mC8XCG0rvSmCRxrS pCJXZMnX74cejTbdiizlZPpxgJ+JuyCOo1S9BLDre7APSADadoaf/+zn+MXPf4E/+jd/hA9ev8Y/ +sk/wn/6T/8pdtsN4m5Av96igcPcNxJFP2p1PKp1QBStjQFwhCdg1jZo2wBArC1Z0CrCL5qwaSzy uy6GY0c2EXsnFhsUbdzMs4ZiLUgrC8HKZeDkC5kArfkas0SwY0zo1lusb+5xf3eHrusxDCPGmMCz Fpi3WLx4Dn95Bnd5gqEVn2/PScJWvEddlrvWlp4i+KeOxywW9U8mMLbXLhO+1SWxoNbH2ARX/z99 0MHLKZ44BEeTZ9C12u/zkD8mu6w9VwmuktCBVDa2c2odkhMiS2O8lqXgVgPR7BuoxYu1dfF0a+Yx k47NGBADErxFDp4aOPaIPiKNEWkYc4EbpzEaec0pX/JQyyUJaqQILEOL2cUlzmZz3Nze4vb+AXeb Hfq+l0BP50AuZBqSPaSBynWDQH0MAQpiORDLgHSETYnBMWp9FhNwmsJr8RgwC0XOrM2v67VMKe0B C56cZyBrYrmw8fExy0UtkGXv5r1Z3dtK3jtIT5rUj1J+vJe0VWxXaFZLuOUCofUTa12uwIvpUdJK obEyjDQM6O4f0D1swMMo2SMEdQGrPlPRSxYWENp5DGBkgfMtBXv93f2/3/Y7v83xbe5X9vm3BRj6 rQNeV22kvfEUvrd3Ov76nv1tjn2+fghOkK0SQPU0xl8qPaqOPwME1FtNoSxD9C6GP1KSGEEk1gri jJESQlIRnRkfxD/vGo/FyRK+CSAHNMFjHEZYMKf3Ad2uQxtmCE2LOEZ1GWgaK0rAFjmnGQiEEAIc BfyTf/Kf4L3n7+Gn//ZP8Rd//lP85U//En/+f/8pztsV/v0f/r4EfkaABkYIPkfXtfMZuq7X/aXY LgAAIABJREFUJkIpBwc6hprjBQA1jcesbRGCV2HP2KebWmDU085chKe4fTStNIgriCpNttYQJZIW JV4vL3a5nyFGx2q10DLQfddht+uQNh3uPn+DuOvRDT0iJ/TeIc4bhIszNJdnaF48w9gG9F5N5eS0 vbMQRoxjBjJWwEqpMPuGj81DeV09XEWs+wI6hzkZ8udK0FjBHfBknzNzNd/HDmOMRzZnQZL18MAo QZg2zn0ry1EQpWtAtPeZWXoyA7fn5FKDwywJZLQg0oHZoUsJkRMaACNIg/2kKHNQcGIuBMCyBxxc hqSqnWjdGXtPALoDkQe7KNkKqvlLlT2jWVsCu35hGqU+gmS5SE2aFvPQYOYDTuZLtDc3uLtfY7Pd YhxHxRCUa0Uk1vBg1cZtriSaXJ8rZ2MUTYyzEC1ujmwVtAUlkriMai+yZokk66JbuUHSxPWBYunY Ax6oPldjREVhe1I/xyhZyrKMizWLS5pGS4aLS4y46xD7EdvNDt18hvnzC/jTE/h5Cwqy7hFVRp7d 1XqHQIoUjSSu3m67wfrmFnHXieuXp1kgpBRCFajVBa+3RyHzWvA8KfSqvVqd9tsKym/rhn3b42ll 6Te7n1gx9nnGsWsdcy+bMHjq+pwBfv0ck7/5GIDhrLya+M+xV0fGNn2fs0zTUe5ZS3DEcsPlXmzy zcCISVSnihIkxkwtuV4rvIVkPTnMrAoNRvIe7XwO50U78SGA0IEgmwQk1TZjSvmcxIAnIMZRskgA 2JAcpCNhYsmnPT87x49/9GN8+O5r/Oh7P8Avf/BX+OXP/gq/8+GHaCIhdhEE6ZWRYsL19Q0eHh7w zot3sFgsgJgQ46gpmwX9gyOIEkJo0LYNvNdYCBIBmOquq0cWpUxuWWxSLdcHaWw2xpSZbYUzkFU7 ld/Z/M2SecLMUkdQhRVFII4Dum2H+7s73N7dYbjfgh96LSJEGOEwhgA6XyG8+xzN83PsGo+o2TUO JDUMkvrCUA2KCoY1Bjt9ump8E/N5ncpZEeURQmY1004A2STDX8byrRhUXszHjsPPcuE1Az17AOP4 JtxD/Dr+nJqsn9daoWMGSJL8ssUqX0tGNpLTqrHSAGtkqYsRncRkBDjJSEhVPAZKajBB9pI8quw1 chnKCYmFAOcd0jBi7Ackjnmj2zgmqYR71hLLp5didkLPi8UCbTvDYr7E17MbfP3113hYrwXAyLdU a7Y6FpWtL9+4Wru6aaEuG+szsUj6AjyA4sbIKe571okMJEhjJ9LBOWLJcUctG/lGVgelogNUa21l 9M0CYinliSE9d7TZnYEkZ4izj4jbDut+RLPdYXlxhnCyAAWfkUHeU8a3quePEF/1ZrPF5v4ePEpW E8fRyC9bLcq3pniA9p7s7YGFXeib9t63Ox7b90/xg29yn9T7+Xj8xXFe9ZsfT4/n4FzmLHNsjG/D /54K+py8NxnR9Nz96xh4Jzq8Vjl/D5DS/vULODE5qupE4Y8ELf5HiJqRGSKkp4bFBJhG4pqA0Ab4 4DEkqdo4m7XodzuJHOcE56WXSErKrKoBM2fFJWu4rFrTOI643XbYXN1hc3OPhgJ+93f+Hj58+R5a H4AxoYGXlC7n0A09/uTP/i3++M/+BC/feYUPP/wQ3/3uh3h28Uz8oFEyBdLQIzQSud2EgJOThSyy 8wBr/4Zcoczw2T7Mny50SmKlaZoZfNNgHKet5GmfawAg5zDGUQFG0TUcOSARKDHGfsSw7aVNtLY+ H/pemisxgODRccI4b9G+c47Zu89Az87QNw5RK/l5pmyxiWBExwCsXGuaPBntUZAh5IkxoiK2eg6O gQtzI2SBXAdHEpf4FdX+jsZB7Jkwyv0Lgyubozp3P7d88pxlE1EmusMYk8nGT+axhqY+qOZstKwW BSv17cgXIQkVtqQaKCckStLiXQFIVHAxcIIHY+YIDYDgZK0DSGtklBRPB0ltFDMi5cwFIzWCpGw7 SMZVGqSrMcckhc4YgAlBVEIWgPdub95YXVYOoQlYhQZN02K1XOD6+hrX17e4X6/BMYJ8ABO0mig0 SwqlnktiMGJlMSiaUg760rnd1xItbdRcTwcAIUnOirk/Ehu4kLVIarU0Zjj5rj0wW4Arw+KCJkJa FYQ8DgBAkntnxVSvkbvkQtNXxeXV3z9gGDoM6y1mZyvMz0/RnMzhg8OYpO5FQgSRtAMQniJWz26z xd3NNYbtFhhHfdhqnTKwoDzgzMOoslTWe5mRX0+PvDjVlip+eTsesxAcOw5ir/aE2YEJ/1tc+6lz 9uMX9q87PX/fcXD8OAZc9j87OrYs70yoq4yYcqqDv0s37T0+rb9p8vrtxmN3nvwtSKGyfOytmcqq DGDJLLxlXpmAxptlL2kMmnzfNw4n50uEZjFDv+ulMpz3AByocWhnLVwT9OYJjhzmbQOOowZjTSv5 1dqJEbdFLueHM+TDAkq6vsf9/T14N2DmG6xOTpCGCB5GTeUTjW3Xd/jy66/ws1/+HL/67BP8+c9/ iueXz/DBq/fxD7//Y/z97/4OaIySjZKkgM5sMcdiuRChG0cwGMF5qXWhTdrqNbYaAWCAEuUFAISJ B202llKEVYHKC1UJL2ZoMTL12TJyWWJih9RHbNYbPNw+4P72FtvNDrEfxQ+tY4pE6B1jbAJmLy4w e/c56EICN2OSFCFiCQLLz2GpcGTCeYpUxWqyh/onCzcl0KdQtMUq7Lsc8iac5Jty/mXpgcVHb/O4 B16y9kv558CPuAcK7ZX0eZiODewmm/TwmG50SQWt56W8cCjrLiW/pyMzRpIISA55rRKAAYygUCUC aAloIUIpcKlXYDVaSAOnTOPnPNesaWDQio4CT6RZWUIaIWV+UQSraBdW8lekR34qxTAmOAFgPmvR NJdYzOdYLk7w5uoKNzd32Gx3orkTSQZWZEQecvbYGCMS9gA4I782oZ9XbQ8EAMig4phlQkDG1HJR rl3cI3zwXtmfbPRlKewZRNqoSlqdVd60shV1z5sS90CFUpkRosOw6TF0A+K2Q9z1mF+coj1dYjZr EQnoGHmeYG6nYcB2vcZuvQYPYynIpopAYbf7e+FQ2JRP6t97R2YAvHe6mb8fF6DfZBn4TTX13/So QYW9rhWKg5irv/GD9n5Xf+8tmFkF8mvsr/DxS5uC8yivthOnDKq69yFfzadXJSqEdwi1Ry1J4b3X 2kksVlpieE9YnJ7gxcvnePn+S4R3338XV2+usNt2GNMozCoEzJZzzd9Wny0Y5CRIMsYRwxjBSKVg FPN03FBGdbCQ8oZvPJanS/TbE+zSA8Z+BEeJmE6p+JXiOKKdtfiHP/kJwnyGz774HJ9++il+9ctf 4c1XX+Gds0t87/V34MaEWWjRjz3CLOD09BSukW6sliY6shTbcmrJqOVfqoQPoZi7JVJfYi6SAgVp PSLgAYxsjs5aPDMoGZAS5oMIsVJc3WK72WD7sMX2YY0UgYYcnAtgiNtmdITOAe5sgfbdZ/DPz7AL ugbQtDXVlizlVO7PqoxMEfukYNSEkA8RspHcY8cxjeEA4StaZ+lNLhUYFVTyhKDd5HoSy8Kw8vPM hwwhCx2SXhmkGnNd8jkcaEcWb3NYFO1Ai7KRSHEHGT9SmUcGOEXY1YwJO5AEIqrAZ0a23sCJZckx IcIhshZdAuWCN5aWKIK+xGPksam0Mzhmlgwzbki9lQA4BXoxSW2MJP0zXLVWBji4Qv6CiSz+g2GW ydVqidlshsVCUlSvr26x3mzQjdobiATUxpgQ01jakVdzay4GCwYvc14FrtYggNXkagGaBjayNeMQ TAhIMbDhDF3ATLcAyvUyMcVi0QDn4muReM/NBuWhps1ZCrLOG0vQbumYCgQWWuD1Ft0wIG13SNtT hLMT+NUcTZDChGMc4MjBM6Hf7rC9ucG43oBGyfpysKJzyuiZqv093ef1vq55b71l5TzotWzi8P+L 45hlZP+w2KOnn3kijY9+XuTDU+e9xeX/mo6pImm3KDDlmHvl+NCyliH1m1QeJqUpqeipFa716j44 zOYznF9c4NX77+Hley+xOj9FePX6FRarBa7eXOHh7gHd0IMd0Mwa+OCrugyy6YN3mLctgB4pjsiF b5VJwoTdhBkiP6r8ZpAnLFYn8CDckcf91Q2GKIWvyoaSghzMjA9ev8bLV69wc3ODr776Cp/86le4 u73Fey9foQ0B49BhGEapEOYb7PoO267D4vQETMBWO7i6ppWx1VHggHaAU+suEaDdFHMJa9gmnUbH 61NnJuQIsOZVBGDoeqkmupHmQw+39xj7AcSExjXCnCIkU4QIyTuMnsDLFv5iBbo4wTAL6OMAx6LJ CLAwrQqZWXpWgXGg1+vgq/Fm0jvm+ngi4pKZc9O54oPbM0FGAHCakkt5vslKL1Nl5k6VadtEu2py ThfEaydZcmKu8170f2bpHhkTq1D1cJ5gAQv7GosVYRIL3SGwKCca0FFmrgGfZGi+vj7bBpZ4GlaA l9sSZyWh1JCxmkqVmMsZJV6qLhW6yotlwFHXzWiXxJxOTmNvHAT0uiTrFAeNfUh5b8JKUWcytmtN pJDOgUPTBpydncE3DS4vL/Hll1/hqzdXWK/XsNLl0dxiFYMr1gKtiquBp1kTOlKfQsCFVdwsdGV0 knLzuPo7qtln4JImAATANBW1omWbUHGDSWClAD85PFd7m21VOGt04ipxCkYM+Iilt9Hgq7TtkfoR m10Hvr/H7PkZ2otT+DYAYHhmhDgg3j2ge3MD3u7gUsp80BzWpsAg89e8ZdSVVNDE09nY1ZefOO8x If1NVom/3niHb3dw3o+Pf/5NoyuPNwX4dkxdPY9ZjX774zedR5FDsidqHli7VkrsRMX7Te3bu29R 9YUfJ2IklqKXTdvg4tkFXr33Ci9evMDpxTmaeSvVf5tFwLP2GZarBW6ubnB1c4MhJjSaJQKopi7q qGQlNA0ARuoZXZRSu9KgqyI8VUIPllqFRIwRPjjMT5cqMBwebu4Rdz1SBIZxRBpHLRzDoMiYe49n p6d4eXGBv/+dD/Fw+4B3zi/Qd708Pif44OGbgP/lD/8Qb26v8bs/+D384Mc/xOvvfICmaUDM2lHR LDKmMarQI1ImVLS6GCPiOAJOrRiErIUB0llV9jyDR8DDod/16LsOm4c1bm/usF2vEYcIHhW0AFJO NbN2uVkEEBuH5fMLzJ5fAm1AzyMiErwLoomSkXwlHIHMUNIeTVodCJdfy6o4VWEMQLKO5jGS3neD GKjYL0bltYgTQUDT0PXSCr4fpdncdqeCnqX3RdYoZQ5DE+CcQyAnFSODR5i1CEGyG5pZC+cgKdLw KrCtUqc20snkVj+N+JGeChijTL46RxX/lUBDzr1RKjGlX0pKh9NPbe9AwR8TIVJCr1p0YuQYjEBA iBI46qm2IVkYJXIatT5Avm5243kCKImrzJOCDLNicLZwHSw0GzBVjkkQMcuAD4TT0yVOVieYzaSk /tdv3uDm7hab7U6yVbyXayR9/kq4GyjjXBZbaecIuJi6tfQnFcuFze6BpUPPlfsYTe0J0kr4EOTc ZHRAqXys218AjlqpLBg8g3rbP6rXsS015XmzAlkpJowpYug7dN0W7XYj8RjLBZbNDHHbI17dIt7c w/UjGq7tbIVD5B4i1Wf8ltaHAj5sHr798Tbujr+p46kYiOlRKw1PfvzU3fCYBWPfCsBc85npxd/W YvDkSCo3xtuen5Uj5SGOhD/IU03dIQVa1N/XTE+V196KRWoe+Wze4uzyDM/feY4X777Es+fPMV/O NW1eoHBgYvg2YNWuEGYNZqslHtYbLFfL7FexgYK0L4D3ALUYGRh4J1q3CcpcygsZYNQHg5GQpP5F BNgx2tUCl01AaGe4vbrC3dUNvAn8xOI2AIAhYg4HlwDvAlbP3slafEriwfTO4f7hAf/r//G/4Ref fYznf/JHOH/xDD/64Q/xD370I/z4936AZ6dn4GieVWPKyqDsQZnV9y6a+jAM8I0GwVQL451X94hU 70t9xHa7w82bG9zf3yP2A4ZhkKCtKEzAew8wUKfiSbouMBAjzQIWzy4wO1uhA4AUERxpq1vL1tnT YPZob58MM4HVBGSbVZ/TZOBjJHwsJiMToP54kiDDcYjYbrfYrjdYr9fotzuMvQQdjt2gpmzWBnDT O1qsjtMCTcE7hFbTgAOhnc0wm7WYr04wX8wR2gDngsS0WPomFWFVT4IjC7J6nEnlVibElcDQNwml TXwuJmHXkXoEUc9VZTavhtEbEYHJSVR1YkQAvVa1axxjQYQmaU0FNo2Z7fbCJLiyitizaFtk57Qr L7NY0WJEGgdEaYKhAn4qkGqGJDfi6qk4W+IIwNnZCrP5HGdnK3z59Vf4/IsvcHt3j7EftUaERqbY uGzCIcBBbm2WCEEE9lvmdd+SIYLb+obYMY2l4HzPXO+iAhc1GLGqppN4DflDaERjKNjWzMAbuNRq YQCaQgoFNLZ/QA6cgJhGgKXLqXcARyBhxPgwYt316O4e4C4vsVydYrh9QP/mBm7bwzNJ/BjKHDiy IlmWdo9an5xSYbWuj9H4byfufnOt+jf9/j7veVuAsW9Z+PZuEZvdbxxhPu8As39LgEF4ZH72hiKK sf69Z4F+7LqTe+wBjCwTmHNXb0Ak4xgjvHfwjcN8OcfLVy/wwYcf4PnLdzBbzKUgIUT2E3kQAP9f /1f/2X8reyPCNx6LxQKz2Rztcg43axCVyTgApB3XiCDtWTVaNOXNuf9wWVTnI5EUt3GNFHtKqomH tkHTNAitdIYcx0GsBSkhwIEiA0PEzAfQKCVy502Dse/LpnIEeMLsZIFnL56jiyN+9ekn+MWvfomf /+IX+OSTT/DdDz7Ehx98MKmcx1Segcjlpk9kLhFlSGK1KMDJWWxAihh2HTbrDTZ3a1z9+is8XN+j 3+zAaq2gpCW9LWNEe5MwSnvq5AidJ7gXZ1i99wJ+1kgBMtIeDZWAI566nCyA0JqRqXzNv51ewyKE iyCp6NWQ7tOFKKYEq9ezIM84RHTrDuu7Na6+vsLV11e4u77F9n6Dftdj3A1ivRkha5pk8E4UR8mW gYdLkl6btCT00A3YbXvstjupjrjtsF1vsd1ssOs6xDHCuaDVWEW1s80BlA1OSvz23nTj65ZjQ/cm zPUc2YEScKmIXuSZCno9J2VwUV/LhAOp+0TOkCwfYHSEwRFGJDiNucg1LmpNhEv8hC2g4ZcsYOwz rTljJtBpXnzp85LHZ5YSYkAzPqDQr7qTgEjvMJ/PcHKyQNNIka0YxTJlc4uYNINDK2QkuV7u92Hl vM1to6m5++Ai9wkxjVtBy77VQy6V9PMp+MgMykDtBJRk3UnWKyHHNGUKokyq8gPkVPQcwcJcUkz1 R8OzYK3fY0rwADCOSLsO2A3ghy22b26wu76DG0ap7OoK/UkVzhLDZRVt6j2eh8nmop4eNTguqazf 7shz9VsAi6eE4GNC+G3f+6Z75tff/I29s2jv9+F1pzFrT4/5WGOy/Zg3OvJZcWsg854aFNTuDYfp 2OprONLMxeqzmjdI4zzksv+JE8gRlidLPH/xDt7/4H189+99iBfvvsR8uUCiBPKAC075gFgnQ4xR BKcnWDjybDGXoi/jCHiSvgLeAexybjY7wsy3YC9FYrquExDinPjAq8NeJWW+jhxiFN+p8w6cCJET /DzgtD3HfDlHmDfY3a2xfnONoRuwoEaEf0zwor4jdr24F1JC00omBQh4+e4r/OAnP8Z/+I//Y/y7 n/0l/vhP/wT/7v/5KTgBz55d6pikvbvTyHdyDsE7eOfBPIDTPikJx2JU6aVJGqR16y3ub+9xd3eH ft0BPSMNav4lEqGpZimpLgho0AciS4obnLb09sDi8gy0aDFou+y2aTGmhIHHqgpq2YysS2fuENO8 7KgBxcScpwLMtN59IgcwCb+wqxJUI2C7vqRC7rYSW3L95Q367YDddivgCECQkGKkGJEtRcnAjMYD qBpYuvIB1qlXgJ8Q/ZgihmHEdtuB7gm+8WjnMyxOTtDOG6zOTxBaLxlPTuILYg7glRgFtmfjsgHN apM122kvWSkSA0ayYk8mchOQgwlrTcnANptSXFch0HVzThtdGU0yXBKLRkMejdZXCCSgK9cAqUz0 BRColg8RZlkQEcH7BkQO7APGQToZI8UitO0SpMyFaiCKbEEgteY4EFxwODlZ4nXzHs7Pz/DlV1/h 1599gdvbe8QUq26lheLEBWFzwjqfXAQ+GTBAlWpatFA5T10qmIIEzvPsppYpRvltilB+OeVVhGIZ AEQZgqMcfFtiKzRzytIH9TtiK3EqATQVOdn7atnqIhovDD7d3OMu3gIxIYxJxg6JrQJxdgORsyVO Kny41K1QwFlKjz1yUD0Zx4+3cXt8Y8yFcYrqtG/SVx5zTxqgPjifjB8dPvMBDzsY76FwPzqmyUmF WdTZT7WrZh9PlLvw/gdvZwzZ+8r+PNSWi/xetqzX4LQ0EDRlop4A2ruBVzloYHS2mOPs4hTvvv8u 3nv9Ls4uzjBbtAhNAGtbdmK1xENkugMhTLRUFsZJTpHtMMIlgmMPD60CSaSbReRRGwKwnIOI0HU7 xDhkzdChxGFopQBYRz9JNXUiMEg1dzAoALRscfH6HQznp/DBYf3VNXbbEZ6SFJfiCEAC+DyR+FiT pJc2TYPFco6T1QLnz/49vPf6PfzkH/wYv/78M3S7Hu+9einZLnFE0zZIzFhvN/jDf/WvMFsu8KPv /wAvLy+xWi7R9z04Mbz3YmHhYu1IQ0K37qRGxfUdNg9bDMMIJIYjDWU0KUZqpUFxAbBOIgOg4LAb B2DWIrUB87Mz9FDC9R6D1eaAV9BDRklFu60YIjFp0yWxtuR96rzWKIgqdClXTmU1hyUGgi/MuhS6 VnM7OQ2DLxmn42bAw80tbq6vsX7Yot9I3EzSdOJs7am0TmHQ2GP8VP62jaJAxLRvBkk9BCtZODKG ISFuI7q7DhSA+zcznJyf4OzyAovlEj54OHIaU1M0XhAXWqeULQvW/YO5WILkDYJo8q4AB8h7UJq3 s42fmRXDQID4LU1gkAB2QhYiCUCv6zoCGCMhEtASYQ4n5ketY2ExGLb4lkXAFEUA63ikL4VTC5hs /uQYHEfEOCCmiMgM4pTBg1VbzYCyZtBVNKEnYN62aEJA2za4ODvDZ599ji+/+Aq3t7cACN4FcQ3G hNA0kslSkByIpbuyBVVncsjAwlwcyHSTNXEFjKTgx+pc5AW0Y+/vDCz4uEDO8RRgyczJWiKym6Ks ti16YdRJ6TTHJpEAUJk+UTjASWpZqMvUZVqRHwk+rJ6FdWZqNMi2bSorsc1ZHt10Po4J1keF+/Td yZ+PulwY2fo5OaOasz39U1LI9Qsi52rrypE14iKj6+fJylK1z2oZ/xioqMGMFSM8fDqqnkEdZQZy Mh+urcYGPKwmkNrBLDAch0CIjHlYuwpD26Zs2diiVMQ0hbGKmJBkKZ0clwg+aeM9Kin5CaQmMYLU 9UkgSEyl1x4VTMDq7BTvvn4Pr16/h/N3LrE8XUpZBq0bRWDtB6QP7aCgOCHsB8mQI8XYBMSknR0h tR9CALwKLGX2noAQPGbzBkQJfT8gjloDA2bytwg49TpzMRLLghtTFG0kBcA3M/jgcUkvMJu1WF/d YbhdY7Pr0XrLs0eJnUgJ3nm0TYPZrJUNHiMa5/Deixd45+ICY9dLSukYEdoG/TCgmbX46JNP8D/+ z/8TfGjw/e/9Lv7FP/tnmM1biZ5X85MFxUUGNusO27sNHm4e0K07xO0AjkBgKYoTtaiI7sAMnEbm zAiQEjy7TJQRDHZAmM/g2wYDy0JL9YMiaJnr/OVCmLYhAWgxn8r1YtqdE+YrFQM1Ut8YujJpZs6B ixLY57N7ISUgxREz3yCOA/rdgGG7w/p2jfvrG2we1uBRuB1BIu1tIyZlivwIM9fT8gYFKh5amfTY HpahDbOUNsFIwwgmxraP6Lc9uocOy7NTrM5OMFsu4Bon8RLOHtiByCwOSXm1pfdOStlU46s3sjFr G195u4CIIoitZkI5pmsq6+cQteJlYonHsNoZZvlwJG4oYYilyierYBJQq2zXQBsBYKEl5zyII6Je i+OQgz1TTV9TyVCECZEwTH1OgpT8PVnMsWhl/y0XC3z66ae4vr7BMPQgiDaUork3kHuEWOyRuR3r niBCB6XSZwUxKoFbUk9lvxVrzIEArIFFtbol8FSES7YE2btqybT041oaGaCQKRfrDiW1OlgPFgUG OTYkQRSAjLDFyuPU4WLAJ2cFcRl7BhFl8PnPPDcowpkngvrweDtgUV3/iYvlactaT31+CRrfu2N+ ppqP6cMc8As+Qp4HYzoCfJ6cg8mHj4AmKs9kVpPJWuRbKwhRq2W2PukaUuWyNW6SrSBmCSA3ua7R GMwKoW4zC1PgTF/C552m0kvBNofIpBZXrTbrxSrniMVSwZBCfswAR8wWC1w+f4ZXH7yPl6/fxdmz C7hZUDk2Qh3L2plcDmeVcSGyPBxOXjFjEpBL7kZWBMYePrTyYIqk4AhoPIKbowlBCkPFJIwEyEV3 oGiu5PbbSpWYDZCT1u9JWrkvz06xmC1wcrLC7ewK6+tbDNsObpSGKs5JdgZBgqDmTYP5fI4QgmSG MECc4JJoWeSQWzeHEDDGiJgiXr9+jZ/97Of4/Ne/RqvpqnlTkhQP6fseQ9/j5qtbPFzfY7fewSWP VqfRzOJsi23ClKVrpqStMSwX0Zm2moTh9wBWq1XuzyIrRkc3ylHiz+uoNOQJPlStcSGmZu+mqZjm b7dQuXEYEXzQ5nQC4ACZZ0ce3cMOD3f3uLu+w+5hrQWDenCMuhbCTPeZ1lOa4t6JB09mqN0ekNTq MtEa9LwUE8aHLfrtDuv1BtuHFZbnK5ycrTA/WeRA5ZRKppMF4WXXBvAIjzHBUz43x02Ffmx6AAAg AElEQVTO4ti3Veqz7/usJ/NTCRGCFOESWlIaQcLIQA9GQ4QAwBPpfCODI7GsZeNScXWgyiJyCviT B7HWC0kj4khIaQRiKXQ2xReU94SBIjMHO3UtMhEuLy6wXC5xfnGOX3/2OT7//Evc390jjmLSTwYK hCCLlpcSxsiZPxSGWf3YmhsjVWA4ncpKIGdxzOW1qc1U004V1GnFtfafH5UgyZYhE/iVaNc9T/rb OqWV9dZ75ZzkLJFB9TgrmjmgE+y//XY84m/6yJlsVP2tr0k3Fuc504/YQIW8TlRDo/L9fI/DtyaH GDWPz5PdMK/EvuVAf2qh+cgwKojE+aGlQR0jed3nBjDztQVAWC2nHE8HlTcslKjGsgI+lK9I8gDl XkIpAzPN/lMX3pgEoLjgkbwHeS9NLYMDggMFsZh6ZtAwAt0AnxjLtsXlxRleffAK77z7CufvXMLP W0lxt3ihamZHDSK39aOsyFTgog70Mv+zETjAqrUn1ZDERO/IA8HBk4MnwkgJnqR3Rj8MxYoBySYR bY4OlrwQC02YjqS3OYTlHCdNgG9bzFdLXH/+FcZth76XNtaevEZkE9oQ0DTqa08J3jupr+CjNFZS PzAgmm9KCb/7ve/hv/mX/xI///lfoQ0B52creO8xDAMYDO88Gu/V3cAY44CRozA6p1noicEcM50x QaskCg+xgEvTuJgZVos9MQNBzPbL1QmcI+xndtR/PxX4JIwtwTkJugvBI8aopZXlnDrDw76XkTgD jgIcO/CofWc1G2boJJDy9uoa97f3eLi9R+wGeIh7SgpJSdAvH2GM++89xhAPNKlqnACk5DRILRY4 fI4xZWExbjvcdAMe1g84213i4tkFVucrLYSmwMsi86DBmt8Y0Lo3Pv1f/N75jaPP9Y3+agXhDAGk rMxmBGMglvLhBDREaKFt3TUjgXIdFAmQTUAR1CSaCiOBSZr5iS7h4ALAUV1jIyFBy07DrBU8neNH wJN9HpwHZjO8ePEOVicrnJ+f4+OPP8WXX36FbtchRUVAlUhO2qxMXrvMJ+pYijx/VP5mQF0h9b7Y zyaohLo9D1TI7ctuFfDQeT++WoWHHf28so6Y4LQCb3nu9LOq6ImAWrZxHd8/dk868v5verxd9sXb HQyUmCZgMtASSFq7cGrBpICEtCjZZJCoceIB+C/BjHIfK9W/Pw6VZgUAwWixjNEnzg3j9OLV09V/ KUiqlB5A9tko6FJ5EqsCgux63Sc8MhhhQeLaGDErYwZYYckFPvM7wRSuKPrBA00A+wC0LdA2wKwF 2oDUCMAg79B6BxcT+H6DdL/FIrR4/52X+PD9V3j+4gLNsoGbNZrwEOE4SbNDACNzLhxnrmPHxVUO SlPLRZ4f1UZqE6QgaqkBkXqGSx7gAEeanqpMx3mHsJghBA/vHHpNwxRzScQkQztrDdPXSBadT6I5 E0BNQDhfYTVvQbMG6zc32L65xbDp0LgExw4uOMzmLZogwIKY4cnB+QRCAKJUxGs8Ac5ht+vgvMPJ bI7f+c6H+OC91+i7nSxkTAhmuuIiuNq2wfnFGRCB+4ERe63W6KR0es6c0UYEEm9CpScCGZx3YBIj 6AgGk7Ryny3n2Zf2Fjr+0cOYrPfi0rCiV7Kuso6TwM7KgkEMBB9ASbRlTpKKu1tvcH11hdvrO+y2 O8QhIg1R/fmFVoicBG1WggFGRXz4XhlD/QB72sResKkxJqMRUBF64i5zqjnKLEaM2A0D0sgYtlt0 2wucnJ1gcXIC51sMaUTU/jnOusZOzOLT45jZWLSd8lRcjWk/M+HoNWuBrbe2gk5RH3hgQgAjgtAy 5DckHgPs4NWsaf5qTQ4rFTMBJBKXHau/lJwEipK2MnTkpC9KGgV0JfHHMix4TEHo3rPn2AiCuBCY EZzD6eoE81mL1fIEpycn+PyLL3F1dYNhHEVIWN+fOAIEDVhGIYi9Kp02jwYM8vu6pwyZWSmPyRpy mevaQrXvIpFTj4GTw3cO5wGQgIpKGzdAw0U6ToBFYsufzc+W78OVID5yz7x36+d9zK/xxDEFjHzs kd/uOtDtk1/A/qsNRTr3BXxNh0ylXArKd8yraOdnQX3w/ULz+XbGdlWuJZ7KH/vbKQ1PAjSpTEcp XAiDoQV8ULlYI5tNx1rmVgqjuQqvGJAt44hEiAY0vAEOsQxCfyh4uOARQiOdy5sA5wMoOHATkJoA +IAUPFKQ1zE4xECZnwQAs8RoF3MsLxPmq3Ncvvs+Li4vMPMJzIO4KD001gsYOcIsUATpjSRNTy2Q vKDAAi5ErT3caPafErloemLFSCnBJwY1DB88GpIyzyMYRI0UBQoBfd+hH6L0HeBYWTCmh93ZUu1E 85IIenIEahycn+F89gKLxQKbxQLbq1vE9Q7dtscszEp9DpZ4ApsI7x3QeADSTXUcBjRNg5TUVw/A g3B2coo09uCUcroOVOswc+pqdSqa3gis79cYe7VikJYIV2Jy+r1czFnnshCSBXkykgPa5QLtbI5I inOrDZDX5IjWuP85V03FjPmatcIRmZU2a1TWztZD/HHi6AfGLmKzvsft9R0ebu+wXW8wdtKJ1hFJ ijCJ5j9pZFZF+U8Ewt57hfT2UHxlliai0owpX5/L30QTQe5AIC+uEfFLCsilFDFuOtwPA9YPG5xe nuHZ82dYni7hQwunnSstSKnSeQ7neP81Tz/b/9a+5eWpI2veBICiagdyYfHGO8SkoIOVSaqmzyAE tW4Ql96uFpzF6teXLBh1MXBJkZYYFpLGbMmra1P6CAlDNiGZCribHCIsEicFth4pMbxr8e6rFzg5 WeDlyxf4xS8/wps3V7i/f5AgaIgPOqWEcRgA8iXw2YSzgoFMT1S/b6hOdxYX4FEfll1S/N3IoHv6 FIewfnItmq6xmb8n76E6h5DTbMncKQaIUpLKr7oP2RQAHO6Tx6gnB3Lr/weg6m/5yNrrBIEZ1ysH VcDDrOlycAYP+es66VmxAEzXr65lp2nG2R5et/vYeZP7VeNiYgl4np4EhUMw137BwPWNtK4JA1r2 Qc/jbJUZGSUPjRycl2QJ5zVmzzlxZTQevhHw4NoA3zQgr0CibST+MXhQ8Ao8COylynP0AUxOs9EE sIwEjFVmmksRPQNLH+DhsV4s8WVDaDHihfeYuQbE0kYAUeIzxE2rJSkghQstFkzxlM5UBS6M4GtN y4K16nLErEgrRQanJOmfaQSlFj40khcLBnlCcNJV1XuC8wO6vpeeJAZUdCA8QXalZkK2AhByoGlS QbJ8do6T5QK71QrrN9dYX92A5i2a1RLJic7GVLRJTw6ukXK7zJXJRlGqJ6lkmKIUxrJ8fQC5twgI 4Cgo8mR1gsY1mC8WuLu5w3a9RRx6gDzAvpg3bdInBK79GCAukaQEdXZ6htl8ga1WcaqDNx830Vak P9nMCTEOAEIR/JqiB3DOZQYRvHbwRAJSihi3Eev7BwEV9zfYPuykZDmAQAFeLTsxxRyESBC6iWmE BaQ9Bi7q45g2T1WhJECZtyJloYWSo22/s7neiRvIwcHBS00NIjCV1M3NZo2h69Gtd1idr3B+8QzL 8xP4EGQjuel49gHeY+swARkHJ2luOdVzYv/JVc01ZZyPoPmHjipaYnVvmFwVF2Yi6VFj5fg9kLNj bDw1aKLJgxkjlflzypTgolq+HFKS2CQLSBQwrt/mYh5WaISURqQ0gpU2nHc4OzvFyckKp6en+PKL r/DxJ5/gyy++xHa3BUDwBIya1ispmYzqQbNKPImLqcYgfMpZbGQR+LWWCGTwrUyvhNDkC6YSD1ZW pOzfCc0eg5Plkzw/qar+CTVxZ2CjAbnGF3UOdQYUyE0xUKGvY/vnyHi+Bdb4bWFJnblmF6yFOhtv PNgk8vwOCS4V6FB2SLlGFtqT92T2nuKV7vCWmS/kt1QYQ98XN4ZcOzektO8ySoYIURmXKqLsJIic VfAjeLDGQSA4UBPATQNuGqAJIO8xa1r4poXzUpHYwAN5DzifQQQ7yspHrpVEdcxKqahBljECzsqK g5fEZ05YjxFxfMCm73H9MMO7Z0u8PFni2WyOhW/QJAYoIfGYa/MIADRlQ8IhyPYo+NAtUoK0Dv1w BiyM9zIkIyNyUh+VWCy80+BAIrSeQLOgJmfA9QO6rhdWyiW4s6w1Y0xikiIzUVtJUWuFTcCIBN96 LJ+fYb6co1nOgJgQ5x6jmiW914j6FJVIPJwGcgLAZrtFCEErKmr2gfYUYeZ8nrkVLGgNLOBpfrJA aBrAATFFrNNW2zA75OgSMtQqqZ+s85IkYhQDWJCqc5ifrRDmLQi91Maowf4h8H/0cFpxc6wAUtaI U4LP/kyChwCvGBlD16Hb9bj64hoPt3fYPKylHwtDa4sARElb3ItwiYzcyEsviRjldaoC2eqfYy6Z +rXbe9CDmAsb/TFwIZgcROrHtz4gjuA0w6F1HhgY65s7bB822D7scNk9x/n5OZq5/8Z6AYn2mKe+ NgvKUYZfr0HWLYtvHspwwcLn5HoG7nkSeGZgdeQq4JiAUc2pzIzAdiZrckzK1yq1NgFzkCeI5Q2w 9EnJ3GDntFw/SQ8cnVMy7spVfIHNhasUFH0mZmkx3rYBl5fnOF2tcHl5jl+ervDRRx/j/v4e4xhz kBobAKrm0rKFkmqBcv3i4igz42qYhr2lyjRh18yAJU/w9BsTkHCwqgoUJuvD0zPUigpmCXplAxMK BGrNA4D5OIqL2uhc57kGGfrfkzET3xYt/Bbo4gBY4BCcwywzR0C7xDxoT5dqILXZ3SjLAjMN2BZb HRXQsD9XKHw1356n+V8RpPFxVOK4qiPm/SOLJBZhVUAlTgDwsndcEKsDNQE0C6C2gZ/N4eYtUlBr eggSJ+FFRjjSqs/kKuBQWUvUHcw6t2LlyhsiAyVJedYChfZw7DKws5YdACE6wpYThr7DQ9zh6/4e 725P8N2zZ3j/5BQr8nAxiaXTea3PZEqGAAxngfwQI8ABuACmG8UWI0eP60eGgIgZiITEkseOOMLP WvjgAY0pCESgxiO4GZqmgfce/TBiGKLGVxiYQfZ1SRp4rKwnykBUm4+syLdxcGGOZfsOaEwYWiel lHXsTpld1DGT9wgQJhm8l+uxMlZ7bpYS3fvBkxJMxjZJgGc0s4Dzy3OE0KC9vcXD/Ra7dQerkWaL LkzRaeEueU9cIjJIFzza5Uza3A9DYf97gmX/qK1M+T1o8FgVxGnmXwm8dBpcJkCp6zqsH7a4u7vD +u4e65sN0pDA2preQeGwtIVFbaaGaowp3ztpE7Yi2HMxJDP7ujLPWTutnmef/pLRQL1pDEzoZiIi Na3LTkpADmwkkjQp0SgkyJe12VmMI+7f3GLoBvTrDqvLE6wuT9R6oWCS5Hs2zCy4ymPk/3P9hicO s8w9tn6wfZD/wdz4yhQ1IscpuGMxdw7M6MEYnfh8g1qlwBr9ngTQe50/Vo1MGBfl5Uz2bI5A7GGB ZqSpzMPQq2VR58gYdf4v7a0hI+fUcIJ3BN82ePHiOWZti9PVCp9++im++vor3N+v1bW3X1xL5iGl VGhP58Kr64zBwk+ymNIpy/SlKzcRfhUQKJfNFr56dfeFTP3ZgWzXMRvosmozZFbEDBKMn5QgU7Nm TK518Cz2kcZk1KfbdX6D49sEdj7m5rNsucm016fmstKacqvrKN8jkQe+DqisLqRzVqygpUeSJdaZ YpbXq3wtz2UdrwUqU2z1fkjL7yervecI8AEUvFojLO5B3pPPAlzj4eYNqA1g7+FDyJUu4T3IEyg0 IO9FCSS1aiiQAGT/SaeIAnmyHMkInibkPKX1ElOXY1dUrmZARqWRoqVLExxGMHYU8dD3uB8G3HUD bnc7vF6u8CzMsHQNnCmTnsR9ZOJAm+3ZXj0KLvaPfdrIWoVuECYGtPJkUuuCTw1ckIBCgvhzvXNw TvJ4myag6wb0w6jZDACRz9qd4TU2pkjma9dJc0ACoVdeTosWjoEdgIYjWpKF8xAGKxqUg/OS1xuH Ee1shqHvQWpFwN5m2W/IZZ8nWyh99mbe4qwJaOcB8+UWV1/fYPOwwdCP8F5KUidOGJMFxkmBq8RJ Ua5Ds5ij0ZQf55wEydgGJdPa3nLjE2lzLUKMVe8PhoIeB8SIYeiwWW9xfXWNu7s77HY9ht0OLjpQ cjkwjzV62nzCUhWVNQOlGCEjSxln6S8zdY1Y3AcA7ZqqtFQxM/s7x7rYOujzT7QQBRP6BQVQCZKD K3PmQNKTRa/NWhw08Zi/57xDGnushxGpG7BeL7DerHD27Awnp6cAgF4tQBkUQYChqwbEVOvLTx+T uKbK+lIAh1Z3hXa6rbgHkWlswlwM88nzAD4xRiQ0SGjh0ABoCQjqC3bkc3nrbBtQYJ9MGzS3lCkV avXzXtxLzCGnc3MVwGO+V8u0sEFnixID4AhiAVfBO1xcnGM+b/Hs2SU+++wzfPLJJ/j66g5dP8j8 khWjguyZlLL2L3UyUuWOIe09UiRbDVYLaKgXQ/jZ/ttUrVOtYWdTPU+IEQWWVDfJArAA0mzh2QOX ZbPbQk/HeOwo3z345PgXvuH4NsDi0WuYDkKivGSWWlvC7Tnzo5Z4KRHkSSu5pvy5kqaCMAUAELqV GiQJ5sIgVG0a7N5AFqwqWXSsuj7ewXsvDbqch3deAtuDpHFS46VhZ9MAbZDfjQc1QXm4Ao3GIwVC dACcQ3JeAjDzBCELXwP29VqXjBlbE5mBCr2jICJ7Va+b8fmyHmbtt/vXwa2ZMickTAC8NNHc7bDu B1zdP+D1yRneP1nhfNailcA2JB7BYAQvoFC6IzuA/CPgwiBgRnMKKDIa3AteMoDBCYjA2CVwinCx ycjNOa9augOFgNY3aHzAtuvRdT3GYRRicuLfteBDu37O488MUX1Le+A5MhB4RIJHA02LU1RLWWAz HDVoVFgMgwQpltUsFz3QLAmKGg1oaKBk46R62WwG33hcXwXcX91h6AZxE7iQ3TvGAFmZJzvGyekJ ZvO56Hjew3OSyP5aszI0ijIUm58JL2IS7YAJKSZ4HxB8AECIY8Q4SlDjw9097m7ucH9zh77vFXQY 8q9aoSuQMGbJUHChQb1Jz4uqiRASmK20NJfr6FGDtmPgIropqOPq2c2qJSm1Cpo0KdxJdRowO012 d1pNVvMmqGg5Yl52BrUQU0T3sMHQd1iv77DbbfH8ZcJqtULjvAR56aqZ28JGaSDDhNRxfa5eH3M9 TLNJyhwUzqLKhhGA0H9Klowkv9WECwIiOcmIQsIIQsMy7hlcZmpMqfATY/QqIE17r12jQvJiwSEn gC3GCBqlUVnKdSbkGq5ifDpw/VWAZoljYiyXS5ycnOD0dIWzszN89PEn+OzXn2uFTwl2i1EDy0iD dbOlqgCcMp/QZzHBrnOa5zlNmWy1ZqwfkGqHUzcE5/9NCMhjHUEsdh4A7H++Dyi40E+esafkvH7v m+js7+TI+JoyIBTkqjTH0D1ohd64yE5oyq4jiRdS8CBZbApyuV7nym0IVZycQwQwMIo1Dqz7RN0W TvpiWewCeemk7ZoG3AT4dgZqZ3BtQGha+CYAQeIhEARIsPfSW8tZzAOV60MLUgFKQ1UQbzVNBon3 8aTFMhjwLfDBQJKcZ1czl4l9l4BcQ4lR8d5aXlL9ff2SZsj5RHAQebXmiE3f4abvcDUOeDN2eL06 xXurFZahhWcPpBFOU8nhKYO+xy0XRBXRV3TDVUwGIftBpUujmOIhcggxMniM4CbBt+IOIefgnbg1 xFIxQ3AevR+kLTcn1Noc9NrIyhRPhCyjID0GgATsYpIsEwIaAhI5NBCzLjEwcpI+ItTIdZxHNAtK pekcM9GbibYGkcxRfNaOEFqHs8szzJYzLBcLXH99jfV6IwjPhcn4icS3lzhhebJEM5+BtSiYs0Ja rP7ZidujrMf+YTEeOfzaaQ+DaHUqdljfPAiouLvHsOvBCWhcABIwRKvSpmBCUwEtot2ETVSmnlhL TXPMhdaI61TUor3lMR4UFZr+nar36rXOPncqDGai+ZMUeQNLN8pifrXAJ5fdQmaBMaDoicT1MCYM mxFvfv0VdusOz1+8g4vLC4RZo6dqvAxM7zeVCrmsuN+zvDx2HLNawMZk61vHByjpcSUoDWSwzRtJ ajhrNc6k5woDFDdlozn0ztahEpJcpeAe0L8KC69dV5kcojPLYwVGyQRu2SSydglZrkC6CEPljnMO q9USH374HZydn+Ly2Tk+/ugTvHlzg91uK6TsCDGJxcJ7n9tIp2qME5Bga8T5v4lFoj4m1qR97l19 LhjsMbH+CIh47DXU1mMgZp8Ojl6SnwYef8dH7UIXMJEygUojOMpKShGZto+tswpLw0wutC1LKDIn KuAwPiwl/VW58B7ReyTvJLMiSJp1CF5czsHnGAjXNvBtA9cEybrwXutEBLBaMEhdIJxdKGJtkExG ytaPTF0siqxjjV9KVMiviu0wJefwKPtv+l7FEybvVG/UL5R/T8SXYWxdp/oCnD9W20/SRpTOIflG 3K6xx+3dNb7arnHdX+D16TmezVrMyFcXKDFz4ZgAzYfysPxAmWZIGQ3l8wyyinkLubJnilHT2UaE pgE1raSFQpi/ax0a79GEgM4RumFErwGUtVlMALAxr/3xFsNQIik2xACGNIqLxAnBsXNoyIOSMEZy QuwNRSHMYQDiWMz3QCZ6e3LZ3MVsnTm+GsoTMdzMYdnMMWtanJwscHV1g+vbe2y7QYQvSYtw5zzY Ac47NLMWLhAGXQ+vmp25Hooyy0ee//DgxBJQRIQ4Jmw3G9xc3eDh9h67+y2GXY9xUCCRRGASMzAk QeB2XwMIWiSsBhdICRFanAxRU/VjTrsDzB9X/pZnMHCRRzvlu1x+GfoHCr2xvac/dSoqjPkyISUt 3GafkTAJT1IS3Sp0giwoy4OiuKRSF7F+c49+3WF9+4Dz5xc4PTsV1xUnJKo0Lx0Vgd9maSbg9dj7 JWKaAVfmxgrYmUm3ZrxF3rFY/5gRNcIdqklEAAMBc+fhoRVuFbwKlfMUvNfAkE0UikbqnAMHiEvK S4ZOjBHWeIiLVM+6l+1S50ktlSz+aKJsrZwvWrycvcDZ2SlevXiBjz76GL/66GPc3tyLiw8s/Ywg FgxwqgKAGWMOqJ7SElAB7+ozAxv7y1ZcJTb/BriQ+d2ktsRkIY8BmBrolHtMBzn9PQE8LOcfAxb7 8uPvGns4IMdnSXlqsQ+WvVEFJ3KZhbyfUwKSxElZaqMUISyZF/DiknDakoKc17TMgGY2g29bBQ0h p2vaa/aElAMv/1/y3qxXliU7D/tWRGbWtKt27X2mO/REUQRJQRJlWw8U1KYlQTYgWIYF6MkQZcKw aKAt/4jzt2zADx5e9A9syG412U32vd333jPvqsrMiOWHNURkVu0z3KFNW3lQp3ZV5RDjWt+aq3u6 RtrWT+VAXJruK9mBAkQ7YX0wfC6aF/YLa9pgodQOv22bqVCcK5Bv66ZwIuW1/r0txBpaF4fZ8tha EyoTkMlvBzMfJb0igBCyOJcyMu4448AZr9IdnqURvz4d8Ml6gyfLFW6XS6xiowKLNPZezYUPij0Y mA3wDDlRGYBgfWUgk6jOhyzx8g1nxMUSBAgDhVYZISDSCrEdwIcjxpSLTV83c7ZZmFECqiJYiDXV NsRPYMzCCMUU0QAR6JqIzBJJIh698HLtYQglkRZMojDpUvoVdFZNy+G/K2JkEvVtuwi4brfoFh3a 5QJfPnuBN3dH5DGJx32Qjdctl+iWC5i+nSBZT5sYNU9GcSZ812FRBpwVEARCHhPevH6DL7/4Ei+/ fI42tKAMdLEBMmMckzrPanCSRgRMfSZK7Qm4KSRpSW1dzFaQJ4+TNs19L+5da3rURY0cSFj/HNAW YjDXZDAnNZWIRzMsORbkc+YMBMnrwaoVYxKv56AFvmKMQGAcX91hOIrGZ3w04Gq/xepqLZFFpHlM eNR1wuqs+Y45qiTUudnN1pP8BqPAYCcEWg202gNUbVX7gyHJ2SQXhuSeaUhiz1MgMRmCERloWPZK 9IgSmgrnDgr1Px13q6Zs8fkyF8m80UqBOCXHpkeqwbJPZ1SPlSwOn8tFiycfPcLV1Qa73RY//enP 8PmvvsDhcNLoJOmk+edYRFcl55xz2XMMcPFw4xbXHa9vQhUdv7CeLzx8rnGYz3/9fsmh/lL7532Z +He8B8h923GvwFk/sHpmfRRsXNFOqE8WULQBEI2D5XA2gVUy6ARNCiWah9BEiahoGlDXgBYtQtfJ ayEmjdg2shabBhQbTTilAEH3TCIqgAKWLyY7byYKYlFVfzPMNQy+12QdGKN2OGtLh6oinZMhk7Ux wwI+yazAwtrrvnKOZBVY1Hve9qXdjrik+j87pmbLoJe64yekLVmLooZMCFm0/ykEDIExUsYpJ9y9 fIGv3rzBi6stfrTb49FqiU2IaAjwdA/zBc1cDVT9DsDNJYbO6z3onRUvffjCYkkfPgjjaCGIkWKj dTSAJhBiJ3kxAEI/jursORbCytI2x4IWX1y3kcSZRqoSakphZSA0qrTTirmEYPnYhTA2Fk7EksMh jXyZCZgaU1dH4uR/M6StIRDSOIKZsFwv8Lh7hG65wlfPXuDw5ojTUbQYOQO7zRrLRadjIYykidFV bkOqUCxN3Xfqw52BlPjlbHwgoGs7LLol2rYDBkZOLHkqAJDWDgksameZ0KLqNiaRWSV9NuBnBZaS LAnVdMwdYYvK/N3AAnAYOV1z+k5c3kmLd3mf1bEz2qY2b2wmIAfkYI6tAST2J/nbnyo+Qw1F5HFE iBI6yQl48/wl0tDj9ZvXePj4AdrVAt1SnJYJ6pzLDM34dc8MTfv8Nge6ep1ZWXSX4qq8M4DMu7up 2DkoEtYAqQEwQhxEMxgti6Nnp/uqtUgesIbAkhM822VGFl2CdklPtBENBXDI4GnUiB8AACAASURB VCCCBMaEjGQaWtGOab/N7McsETtibpG05Ja0q2mihAe3HXa7PX7+87/AL37xl3j21QtJzU/F78LW WNM0Yv++OLZVuzHd02emu7fNkWniZvCknFvRx8nTGfXmfRvAsMMl2ov7pLx/G86Yft/3BBZvu57V oTwHLTvPjBRY/BU09F7yPEikBZtZIgaEpkVsWsS2RdO2rpEQh8lG8kE0Ch5i8OgNJgnHnkRUwOT1 ivnqfwJkgub50TFke1fwUI2HrxcWs0YZ8uLgj+opc88x14C5trvCC9XFhKyhuJgCUrvc71Vdb5nL LqDKYjrViCuo+581xaCSgYzAohRIkliyoYiWgpQYSAmJJFHXy9yjH484jQPuDgd8erXF96/3uFkt EbnO0JmtN9KS0hHv/VQtzbXiENXvItFwFiYEKp67OWfwOOKUDghtFGeZrlMJXZhhDITNZo1uHHE8 nXA8EYZhmJRoriCW/28KDcuvwQQNndNsaBrCNrJUXVy3LUJcgKKCFC8kA8T1Cv0wACS1UWqpQwJj ZrU5YN7xZDsdpl6OaproYsT+Zoflai1ahC+f4XDsMXLGcr1E00kqdahnfAiSMTLFiJSThxPVa8dJ mQlSlTRIFN3hsm1abHc7jENCYMazXz1DTqLNCYp6Bc1KDgvNGVrABQxcFCdN11hw0uvh2g5p6lzz cQ+4MLWBe8hZbsZyFHNBsV+KI6eCYt9oxeOcTFYmAlhCpiTcrYROjsxeJE5K02dkjmK2Y4BTFjuC RCxLTozjAce7O1xdX2H/YI/NdovYlrLpnFGyS9X9LLx6yjxsDfH0XAf8gTy6wuafjcBU697HzMaG xKXMtRwkoCGxaPJaZfZCnEzCk1wgRuvcFu77vgBqAJMKqoQg6fIDwEFMJAkDMCoYIvMiL8KIRwkE /0JOUR8MInFIXi4X+OSTj7HbXePRo0f42b/7c3z++a/w/MVLjOOAEIKACtuXth4xk4+qPez+Yvcw ZXdmvXgOFyI/YTwuAZXn1tfqXpv72hj9KAJKYWLvOt7B57/z49L4JGMcCu4TRFvGXYdms0azWSEs FpL/aNGB2k6iMRpxkkQUzUPQ8gUOYs0sYmDXtj5ZhlrVGZls7OMNANMCYXYiqZDpIZyQ8c9UoMHc N0wrG2GGFKtFRs7vz+Eln387AxfBQmm5zG9xPubJe6H9Rvy5Prl6Mpx2BKgAXN3fBHXRJjESZS0N YFpw2b9Re5bBQOjQhwHPxwH86iVoTNitlrhaL0VhALDXhyA2x7cpgTeM7i5s/mNh7kbohF1IrJsM s25E7QwnRsaAnEexoWZG07VoQiktzhFiQwsLxBBwCEA/DBqhwL5ByYgj15uzEBcCZHHroCYAERlD yqIOQ8AqNuhClIqppOWfQ0DbtAhtkGiPfgQncT4zJ0AGJGEWSahrtvWVE2KQyZXQP8AyWrRNQHPV YLHeISwIXzx/DhoT4ipIXDfEnyNQkHh4AtomgrkF0ohBYzhJVcESeikM2WyXsnHMs18Y+ogBoQE2 1xu0XcSYRjz/4jlO/RGLINVPeUjIiRWgaT2UzBi1TkjQ50ghOn1OGoWQKtiw782cxLly9JtsxPpP 9V9R3ZxigenGMMKdeUL0AWOkNchTbYVw+wJIQUBWhy2S5xIFsDkQc4CFzo3Z7MT6TGPfTMgp4fX4 Ev2hR3/ocfNwxP52j3bZ6qZTp2DVQlmomxOBagCcMdv+019LMiDWdSRaJfnNnDRpMqbZiCSMqNap shjwBDdiux4z46h7YiRCE4A2tOiQ0Y25mEhQrSua+mSYLkO0P+V7YwqkY0+jgFKoc58Ho6kGwJJ3 scJaSWZHJjyCopg/dtdXWK0XYiq5vsK/+9mf4asvv8KQsmqtZF+IWVbu5RIoFUe8MvK23s1HSMeK g7OiQvvmbML+EIdmD5FE7cBXnlQuogIyAbfPA4WOzhnD7NfJ7VwaVuAmP5RImflh314GJedAFzjf j/699qeCyo502eiYnjeCELoFdp98jM3jB+CulaiQGCFpqqUMeAqAxEoUYGL31sdNpXYYyCs9INge r6FlGb/Cv7ga7ylwDpN+VSNExcxyeRCrXX72e4HidnOeT6t1sv4IVBp760PwXk2ewLNVQmQ4D8aR iQoG8VwX/jzlpBzRZKDJDCBJ9VN/omh8mNW5PTB4HNC0EU3buFkm/uRf/YunRpSC1XS4BzIX7TSd 9WoCRsiIKZ39bojOwkvtRcq4g3aCSDJHSmVP1X5wbcctDTK7a73AQrVLjdl41QhSBpqLegnQ9MxR VXq6SUz7EEMUh7qclQlR6Z8RQm1LYPO5LW0QIpoFlsaA2EVQG9BuVtjud1jtNmLnV+ILKsXgJOMh tJ6LEmOqbPMS6F0YrQMwGWwJo5TkUW3XyALghHEYMY4DeBw1AYr4I4wpSZIyJ47S75xES6EJTYCc DFGhhJ1mR7rMBi6yt8lUyrAt6lLE1BmSz146l29RIfu1lRnGgQ0XByj7PF2cFQFS8FHvc+ba2x1I Y8bpeER/GpCTmC2aENEuFtJffYatZU2TWYARiSwFiONakXWgjGq6lgPUwQqEsrpq+FCTiFBtYZrs Z1+3zoDFgWtEwBgCsubGiDWjBMHTiBv/YFQtmLBQJ8CkZkaTPgXMUbmJjitpn11W5NIv1OCRgNgE LFdL7PfXuL6+RoiEw+mIU98LYOSyL4JqpEy7xYwCGomqEZuBXyWeNk52MVFlhgXgxiefr5rNVkSk fs2WXQGWOpIM1YZMX8VUMf2e/Tu45vgedDK5+oN+O+Ng51ext7Ew9iieE0IqYkR7vcPN9z7B6tFD YLkEdx24aZGj5INIMSDHoCGkFb2+j9FXTZmMChmPYcxH8q0dd751udvvPGb3eedJ1T72teCMp9LY UO0OXdbDpX9na6xas6ahuTB903aruahhNZdSxhgl5bf8HhTIE4CMjjOuY8D39ns83m6waiRBWPzJ v/rjp9JYOOO9b1TfaYurG3zhNqT/lckjsPo3sEo1RASKUQmBEIgYoxR3IWl0cfgzglcWtN2XACeI 09VifWWklDEm0aAwAFbHv9CINwuzgIW2aTWbp6qIrJ3QwmPMEoZrWQxR1O+shNnHQsc3NlKIplsu sVyu0DatAxnovYQGkt5X81ZwSb/s06FcMEABkrYvoLTTHR4BrJYrLBYduraTiqenE8ZxFNtoFht5 yqOCCTWDjBL1I2mLkwMGN5Fw8c1gtnDWAirqNczu11EBL/+LnRGAWbN82nfGnEu/Sd8dpJw5ohZQ wyBvs6muyzmowA9NgZCqpexZtgDHYcDpdMLxcMDpdAIyI6rXegiG7guADfXih5GU2frUh0w0HTPq aPNo62NGT2BpeO1x8xoO5XEG0ORz1vGOOriJTAtUHPOEQPHUHFP9YSYdJlmzQWsiWD0YIZQFeJlm gNy/Q1by244YA5arFa6vr7G/ucHVeoMQAsahxzD08PXExT/FzSw5gTAHA+zXUNUKqn6bEC7U4E+v YMwmrNqfNsgzIEy2vnxhzSn/9KALQh9NgOP9oOIbHfeAC3egrgCg/W6aZApB/Hu7FlePHmL35DHi Zo3UBHCIDiR87VQLStZFKOsdRQR4a3Pfl0+9Z3e/66Nebf7dvA8XGvU+/Zycc482a36wSqVNZqkj RYwxsrbSEIas78gZS2Z8tFnjhw9ucbNain8GA/Enf/ovn5LZrMiktvdo6Ft7NHm7+LuE/sGZkyXD yZnF4xeqkSAZlEBiV21bcROxLH0umSriAsHtZ1wRrLIhAHgMtTBCqzCZVF5tYkQAibaCBH2TorUQ o8s5ibOqwCWctIQHkiNA7YDTJltIRuycOWiSmaj3pxgcBAWtmGcZKa12B8FAREkOZfNEF5csdFwj 2rbFen2FxUISd/XjgNPYox/HiolmjONQNBas1VVnVU8zyuec2R07XaswW1DF/4KnRNe0ERZ1YqDB fuMKpDGm113QaJzbg6ej4dJF/ZwJ+SrAgybtyxoqrA6J44j+eMLpcMLxeELbNCAKUg45RFd923wV XjUlyPXrbUS0Jup1txxATjD1OcOqtW5Fk1GZEThJQjSSED1hEir1ZDE5usO2jqM71UKJ02TTYQI2 2Psoe6P01dbs2+mMVx0OhOVyiZvbWzy4fYDFosM4juhPElGSWTIGThzWa5AmN/MP8rnSWuj4nYHA iQx5DhIt4ZNfX69V7wPXl8w0J/cflTBb2lE/zj/T+9Prdz70Uhvo8o/6lbchRPTMwKLD/uMnWD+8 QV6IScTWQzS/ClQ0wynY5abU0WHz17fc3d/Y8aF9uHTeRcffDxwTc+h2T5bAaq6yb+U5AYwmZ2wB fH93je/d3GDTRMQsibjiT/67P3462XB0/8TNG35vo98CLurFYc01IpiTZnxMyZl7jEHlGNLiZ1Zl tVSmc5U8JrTjXMWlH1zzoMlRMiRUb0RC0vzo4mjWqCZCQE4gLXFrTJ6zOoYVICC2aXVc83FwbO7P lz/VeRMETkn9OhhBQ688QylQNqBJ2QBM+x5MBcvWt2qhzBebAi9kRmxarNZrLNcrxEWDkRNOp15S ouvJgQC27Ii6oDiZ2j+79MmsIV3MpQ7GDDRMGqEg0Bmg90tNKuDJ78avClE16bQ44pZ3NTLo3/50 vdadqhQNnm9Hvb9FI7lWQ9dqZq09o0YKBvKYMAwDhlOPvpesrE1s0MUGMTZuTslc0hS7ltCY81zy 1N/OCKzuRXNKhPXbCJPZGrTxzler34tp02GGDoc5dInmwuLESRtJxmwr5sJVH7w/BsSMbaoGLsRi 0rMzypLQZ1RA+dIh0SaFCHddi+3VBpurDTbrNYhE83Y6ncBa/l1antX0WMCoza/Mo6+cs/VgoYCX 6Mn83DqKxIHejHG8vYfvd7yv5Pp1GO60vZXi3eamXnt2TX2tjmMmYAwB8WqD/SdP0F1vMUZC1kRs gexl95V5AMrYlR1KM2BzfryPZuOdff+G13/bxzujyr71o4ALQKow51DgnuJAhJyx5Izb2OAHu2s8 3mywZEaTgZYD4k/+9I+fVvfEdLm8Z1PuUeHcd5dgKLU6XTQEcCnSVNcBkj3RwIRFgbgfhBJlU8fX tQbmdi8qD5u8rExtMrYzCMjIgCaiklhrUxGLJkHtyUroa1+QugiNPM4oTKVOtQkKVlxKQFVWTQrI cKKcGx1NkuS/B1UOripfmR2M1MHpnkVJMOmRgEDolgustxu0iwUQCONpwNgP6IdeNUwkpdRzEudV Lx5VnDVN/V8YcXn2/eF8U02Doy4XJuXeRupLLHllytDzhVcaw5igKweek3bVC6QCGLaWuTaDGIAx UEgKCK1CKMQcReqOfXc4YDj1SIMkFAtap0AigCriDLvpZHKm7cf9+6jk7amZFrnmqDzD3sm6e+lu djNwDEi6H2RdG4CxeH4q/bAGToacdcxUu2JARidSxqE4fLpvUcXM3gUuBPyLsCF4SaodX+922N9c ixYjDRj6Hub4bA7bBlZhHiyKdIyB2rMvRonULbtM9gqAv+e41LuJEPc1GcbbgMSHSMXzc7y9vt4q YDFrd+0LwEQSGtq1WD+4xfWTx4iblSQ0VFpVoG2hhcVPgHzvnffvsq7Hgew7e/mW/n+Da7/t410C ff352wpHrncBUwmBt3El3UshJWxA+Gi1xo+u93iwWGJJUjQRYxazCFBJHTgnbu/VoKqTZKrUezQg AGAaB9vUgco1gUSST6NEaUjVUtEcOJ0KkrwnxKDJqDRPAVc7wAdriq5dNY1CByxHPLNoUBIzRpZi bAhBqtqR2KVDJGUYUfvKcB+EGfMwci5/C8E3YpoVEAnOyBqALFUnc0qigtZ+RypZO0OMs0Vl3Qnu wDmR1p0fGFMQ7QhRwKipuptFh+VmjdVqhUXTgTnjeDyKwyfE1CEJtBSCVRk8nQFzMYkUkFHGoDAd FCZvEqQJ21zAmM8Zm21efS7YNAvsbZNx5ApMcHlmNQcm2bC2z7UYPkyS2GcieU9wDHuuBPFFZpe+ tGSFJAYaR5xOPU6HI9KQHBQ2MRaJ3YigbQlfMxe4FmH+rcy4M8Zq/OyWRvSr86XbPNkfdV4AJolh zwQwk0anaN/9xuZ0p2ttttfA5E7NIPWgIB9ATx5nAEGS6QXTicw6fn4wxBQJZvEVSgkxBLRNg6YJ WK9XuL65xm57hRAD+v6EYewrqdaYoI2rfjs3Z5RdVc63ca7GHTbuij7Pzp28pj4ml4769/Pr9R58 3j4TRL7p8S6NdFm3l2fL/E8yEcZAiFdr7D56jPWDW/CyQ46xrBkGLFW8OeAG5QHOE+4bqlkzawHh /y/gYn5cAojftvaCIPtX9jcmgjL0uwBGlzOuQ4Mf7q7xo+s9tjEipBHDmxNeffUS8b//0z95OrmS 7Pb3H24vrIlK3UEqzPVt19vjRM1r9KeSXJUZWdEsQ8XBoiWChL1FZ/Jyx5xmGYXqZwFubnAbghHK SpXCJPkwBs1CKc9TkMGC3AjkGowYJB10ThmsIVgWAVOnVwaEIZGlQNfhMiYnzCIDKYOT5KUIBqi0 70xQ85CWtWeWmgtqpoBhJ+1nHXXiWh3tk5X/zpAolq5tsV6vsV6vQBRwOL7B8SSVYwMB4zjCfLgl KqOaM+9nJUnPpA07l2fvkzVUJVuCMf9Kw2EAs2g7MPl9CuBrIl3CC8u5yhCq9THNEMreBPvMDPX5 UQZDYZr1TosXpWFEr4X5ck66TqTiohNm2yfVBr5v982/dSZnv+ke8ggJa9IEUZU5KcS4RFCADFAQ 3GMd+h2LXxJp+ye82IARS9KdaPeroM18XQRNnEWh3MiddgtEujAQZf2h6i+r7xYRoW0bbLdb7Pd7 LJYLjOOA0+koe4/nBLnIaqT3t/1oYzw5e07MK22Yt5pUfVytrbkmkWj6mo4Vzq6/dJ+6fXU7v4kM +05zt6/d8/N0FkEkkR89EZrdFtcfPUa7vUKykuXeZouCKg03zbADpottnLZp3v7/r4KLd2mXvhsz SHV/QM1SSj8uPk7A7RqEB7HFD7fX+HR9hdj3OL5+ia8+/wK//uWvEf/1n/7JU1CJKQdUWXhPJ00l +dYFyHi/BDDG9GppCwCpNG9qMTN3sDLQYPa6QBNNRtNENE0Dy/NgORYCPL2Bggu4hCvtKOTFmDIH yfmRWHwJkpZLB0FtuAIWApGn6nYmDsmfAM2AGStCw6autnMNhVRef4GBqBI6p1FCRbUSZBMkPDe4 9BdAjYAeARlJTTi6CEIh2qj6a0yHZMKhbBMEoG07LNdLbLZXWK6XYDCOpyOGYdB6LAHjKJlToxUH Y9MalHksTp/Z+z5PSGP5E5yDV0R2ruabgInZOqp/J+OqVBg1WQ+5EOuy5ItqInOJYgDMybGAv/KX taOAJAc2zLI+EcApYxwG9H2P4dTjcDxI9FHTCDhEAcIObjShm62nWqv3tsPV+tX6svEp/GCqyXBC bOXkCW4G8c8ws6GlamY1JfIkv4RImaSgWhLwEJWb2J6m+tnBCJmC91CcKd2cZYQOULNnaVzQveDr HfBIqyY2WK2W2O/3uNnfoOs6DOOAcRwxJg2D1qeZpGyAWTSGZU4MABQwai2rIrds8XPpZR36R9Xn CViwdfQexyVzxPyofT7m116Seuff38ecJ+3VTl+8H2RNjAEY24j1owe4evwAcbNSrZjQSB35yVgU +Y6KdtrCmeszyMaiXlE4A9AX+/bOffTNj4kA/S0Cgt8IuAA0JJpdq1lntg1ECDljk4Ef7m7wg+0O zd0RL379a/zqs8/wxedf4O7VHeK//m//66eCFqlKelImYJ6w6IMa+p7nn0eEc7Vw7FDCr1EVIGi4 n5oNYrHdxqaFJ+axsMjZAKJSwhIqhGzAQqWpDCDnJP4YrNESEGdTBxTa1yZKhIARW1iaZt8xRfti TDShIqLWnwxEtgJw4uhZh282sSn30kgVYVQQ4mhEkOFaHmOqoUpWZvdT2u5EOzZSbrdbLLC93mG7 3SK2DU6nE94cDq7tIB0Pzllj2glpHPx50wiM6THVftWJcu8nsnNgcu/vnsaSJrc0c4kvS7NDTHGC R/2gGkfdGM4s/VlEE2bsz3CmIgwrpYRhHHE8HNEfTwriWl1D0dd5sKqjBlh0vb2XrRy2hqvPhr4B B5qTa2b3lXVS0qoDmj6YBGxnIo2qKqm+pCiVahWhtRlcrVHAdJmyOUtV04rlxSDZVwbi3CkU7Pd0 EDHvPxXgRwpYFm2H3W6Hm5s9drudRJT0PcZhdPAGE0KaUA2k0A8rtKffnI13ASBFAzLtZQFWlw6y gbjw831M8m0HXxA37wMVF591uSnl/sbYaXpmERZlLodAwGqJ3UePsXxwAywWUuvHCpnZtQUvlO2o zExAX6jGIMzaNtMazYAREU1ozfvuo2/z+H8DXFw670NoCAHFJMKVvxvJPmlyxjUafLq6wmZIePXL z/DV55/j1YsX6A8ncALi//Df/Mun0URZLhRlDijeqbF4jw7W3uTOXzH/XLQRZe06N4CE/lmeBVLn TlKCLG2MsVX/hAJWjDmbmtv6EireFpTL5oDigyGPdalJyktLKGgMlkzECFkUMw1F9w+BXsdKQEs2 Qr0nKq9/UhVhliRGkhTI+JokpTIbM0gSCoXo8ioCEZomIGutEx/HavNOJ8S+Jx8nAVdBwQ7QtA2u tlfYXK0RY8CYEw53R3Hw1GcawAgkoKtkPLwMFApjnmoq7LdQtadeMkX6xdn38hv7ppAxrquz5gmI sENXt4yfL8KSi6EMVWF1oNI7Vq7A1b8JgazYDScJ7b073ElKe1Phx1ZSjns+jQr2Tta/Esv5/pn0 BajVhlT/pwTbibARYi4s09aL2e8trLqAC7Glm+xeNAvkIdsupMCEedOyGeYjW9QOHOw7UhNjoOAO oK5xIkw0HaWP5JSwtCeUomZWcXW5wvX1DpvNFRaLBVIe0fcnLXoGr50z9dMxx9FQ0bOy56b5LS4B CouWsTm7vIjt+3dTV667fuEoDF/WdGG4kz1z3oSLv198ghJnZ/QVXzCAlUEYAiHsNth+/AiL6x1y 0/hOC0QTAH75idU9qcyDPM+Ggyfg4VIp8w/VIrw/h3v38W1rGi5pri69vllbyhr1sFS2VBUiVHYZ 2HPAtk/IX73Emy++wun1a+Q0ApnAiRD/5J//s6dtbGXjqmAgqlOeNPZDPFEvXVNLUP7d/Dp/58lA uI5B7+cREiyggbJmMDSCQuZ5LH4J5mPhavNZhktSwmvMxQq9lHbZ87NrL3JOSEnlN6KJFGXlhYPo VoVh689W14qJHFRMpMsMVS1XiYqM4elzh2EQJkyW4wIgYjRBzDPRvK71gZ57gqw6KIp5pp4z/S4x az4PrTZJjHbRYr3Z4PrmBk3boh8GHA930o4Qi1kAU/PHJMeAzacTA778/YWlNll/XObMONjkCVTm urqBzXT5nurfjJlW2orq/gVUFEgBqhhH1U7zv7FMoYUVsTuADkOPu9d36E8nEDQiCloJN1ua9+Jf NBkLX7fTo8zl9CWRRFMC7kSqDCmAokW03yyqiP1VfTaAoYxdQIdl2TWHMBKHaCrMVhLCVW3RRp+p rwMq8x9m40DearKUutU9amGo5JQICCHi6mqD2wc32F5tpMBgThjz6CCkgkzyfLOp2ig57bXnzWei AsWOQOrhv7DAqTq/1o44nZruJ5vb83sxaCLpnwOLmgHNf5tH7piGYsq4SId7apKw34LWtembgO5m h6snjxCv1sgUYNkdw0y74rlR5u2271D2Q20S9iEFVxriWR9na+tdvOzbgAMfKoh/yH2/82t1zRrl kvIgJclZZMYqE7YDY/HmgPzyDjgNoCpXVcqM+J//+MdPAwOLrkPXdZIdE3xp+b8TLRW1WL31CzE0 4uBgv6zTqlNCgGCLuKKkBFKbv5LqzJIbIlvNguKgGEg0C02UXBlF4mHVGKAka/INbBKotN76EVAT GHlPmb2UO0Ol/WAbURmZhaxG2VTyXFFzM8ilOP+n0neoxoMs8yeVNgGMcegxDgOQs6SdbiIaiL9G Exs0JIvCUqsLjQiaSpydiRqRBISxiLbGyqnKIiMCKAY0bYf11Ro3tw8Qmoi+lwyVwyBaDApSHbTw 7CmRLBEY+YzITsBInX/dX7jwXU3cy2eJGimPqCz3xuLPF3f1BK7OhYOUaV4N12L436btEJ+AUktD v2MGNB1yCFG0GP2A/njCcOox9AMiBSyXS60MzC6VGWH1cSqo+PL+mdEQGwdnLOVb/Ss4YCs4V9cw 1ffRf2b203cpkKZ1SrR9OfBkT0DHBlSFnsLWXt0JFB6ujMQctiejXnd6kipZBAzzfwnum6VrRZu+ Wi2x2++w3++x2ayROeOg0VGmhjd0aXvFHU2pPK40pezjc+6kZ9ianDDq+4/3kUAvn3OfG+Tb73/v 86r3GmzYYNZzSGrySETIiw6rR7fYPH4ALDqpYxEiovlZVGTeFh7Nhm8CMrQRlnxL6H1ZSyZw2nnv Lw5f7u9fxeM3Ai4AWPmOUEV+5QpcbDKwT4TloQcOJ6kzlcXPMSWJtIz/5A//3tOh71Wtrel62wAr X/11YmeNOfqVNYGqN8b5z/oFw3W/9j+hQrTkixPQGiGZVfsAT5rjTlkhgCLBSvRmhqftds1DJYnB ABKjEjqU2FAZfJBI+SlLLQ4QqVlEdggHeaY5JFn4VarCEWSLsmtXZMNp+pILaZttg3vSrZwllwID DRFaVSk3ZJEJRT3MDIxe7KzSFMAIBKvkKaYVsG5kBZxZd39sItabNXa7a7Rti9PphMPpKGG6BoJw vnbK53zx+9Iemp0/Wx7vIrom3dmskf1m4KYScfTNJEIBNzb/xXGPy9QX5qvrD6jfLWKH3UmwqECU 4iUWvwLV+AynHsfDAWkYwcxoWkneFpuoGVuhicV07i+Bh7Oj8pWyz7Yz6emV8gAAIABJREFUK4lO fq8Fg1plM6X0Uzt5QTJWfXUkSUaXKQshCgK6oQmo7HZmvjxnsFzMJrD5VwZS1yhB7QMhLTNlE0i0 dsUlhmBJ9rzrOg1EAav1Cvv9HuvNGswZJzWT5JTcbGWZen3MDOzztA+FYqFiwGV/eV9n9PBD1Njv xyAso/D7AYz3ATrhHE2d993oKAOIEfFqjc1HD7G83SN3DTJU8wAqFTkrYDEdwArM2LtNHhUNi2mr ay2KQULf5R/IkP/9BhesfKeYih1cBCnH3jKwScBVn7G464FjL9W0U0ZOrGUkMuI//wf/4Gnfn3D3 5g1OxxPGPAKNZIhsGkm1PXeke1sj6/VRT3LZWHR2/mRh+cY3GFur9UgTFdXPIlAW4gtTQwdjcDDu LQQhRo+kIE1DXLQq9lw4WjOzQ2lsFTOgjcoQlDakrMlGuOB5NYuQE8ci9Xk6ZX0FUAmRI/Yqly71 wYxDItGGIAACWaIRUt8jMKMJUdoeAtoQPS+GMwEFF0X6ExV3cMlBibOX2VUQptw1xAgmiSjZXe+x v9ljocW6+l6iIhqqC2eVdePvxOffTU6+vLbeH+jaLGFC/OqrJ2xXtRPmj1P0JuZRITk0TK9VaxTs cXZuHT5rvgf2DKoWTwylCBoykNKI/jTg7u41+rFHQkbTthr9ZMmfSnr4yVFo7tsHsEADFC2EfWuf tUGTPAuTM2DREAQBOhkQSZWCVLaEOHyapGN7kVTyDyjaH8eD9QRVRd5snYqZhDxSzCRk37h1DxX0 lSgDvW0gVbYwapNc17XYXe9wvb/GbrvTIn3syeJE8OCJNc1oi4/nnK5xPR/V91S19z6QMWP271Tj n2kg6jD3twOH9wEXlzQctQar3EM7xYTQtmh2G6we3aLZXSE3EVD6HfUFGB2lWpaE3mUCWCZ90UXh AKNKI25CHF8Y2/c1Vfz7DS7gfDHoPFmxxEwCLjpmLPuE+OIO8eUBpEIRawqFlDPGnBD/yx//0VPO GXnMGPoeh8MBwzgihohOi2mZFGQbdRJORdNGwYlVJdiVHXi+UOu/K+GuXrC1JGtEztSetbQqdD8j DWORXjWEiYkk+VXToGmiZLkkqA+FMQt2Jl+H4xbJ1xhw1JBFUp+KoGiNMabRHSvI1Ln6Mkm2aVqj nyXu3kdDkoEx2b219/puYYmsfhSWR4NzAg8jxn4QAowquY7lxIiC8jMLUfdsAkYYqDCBQKXdFoIr 08kgig7iuk488fc3N2i7FmNOOB4OTogdxLAx2DruvzBlXwM18dVojvuIawlz9SeBSKVUyg7GqGr7 ZM3oOjKwwL6uCxOeABV/VY7B9bl6vbQpg5O1r9YKlFUvErJU2Q2a+XQcR7w53OF4OiGPCYSA2DRo YqO5NaoxnR2262rLyaUzJpody3ExueF8vHXt6Qavx8/CUsUBOoBDyYdRl8uW59Vq9LNWwUZzMmZG M8jWckCMjewtZaI2F9AnhInmAgBEAxFi6RuDS60Thmjj1mtcX1/j9vYh1us1chrRDz1SGmXPQLOL 1q01oF6P4Rzs3DcTl+ZwNnHztX8fezg3ldiL773qfTUb888FXNH8ZKfxsVsgrtdYXO8QV0tw1Hgy LlWjWQia4RFvaVQBZ2LccWGzei4BViDSzOCR1Mk9FBpTEOp9vayE57K0/Xk1bZ6/SPnF5Ft6v7H9 OsdUq/f2c973+9lZzscDo8wVSOqLEKNLGc2bI/JXrxDeHBFylnQN4yhmEUh+qPhPf/xHTwMCKAEN AsZDj/E04HQ4IlJE07aIWh+BVSr3vBGWtCYQQhMlFj5lzxroUMCQbiF/mMyh/aL5EOpzoVJDPdkV Z9Kv9G9dSJytmmfWxSpaA/OAt6yAEk4qoXOcJALFiKA5fWYUHwV3ImJbueY4pWYHiGYgZcbISYir 1kGJMbh61p0uY0QgqGbAIkome0eZFZdPtskqJh0hjItSQk4DUhrAeUAgSNbCULz4m9BIZlGWHAys haiMaLhjZtDFWGVTBATUSDirnkcZTddhdbXE+voK17fXeHN3hzGPOA4nZJI6EJJpNSOSVMwT8Aav J2OEPjNA2TattakeDXI7vvhuqP8GMYhU00L2vWhJ7F+29VXnZZj8q76j7LCnhFUqFbTAE2NgGqVg q7tI93aYg6twfWZxBM7qe0IArGggMZAHRjomnO56jMcRAQGLdokmNpM9ZXMAgmvrmLMC0ACvmTHr I6q++hkT8ArVutAUQBFgyePMF8cTTqkQYlo9iyjJLC8DRWx7mmK1+01jJGuRVbPgrXPmIDkSBMRE IJJEaAVVtWcGOLt/FKuJKivaKs/Xkkw6r7L/pO9t02Cz2YgWY7cDRcLhdEDf90I7rDYJiqQuNCAC XEBNxWV03KaScw2i71+H89/g115idS686WLU3aK/VaDe7zNnUKXxl6T84kRZaNHkn2kRYgMO0R18 AQ3dD1G0ohASKmaz8mxbd2Uupf+2FjOz3DMAHEWStr0fSKr5RiI0QczDMZALcDUNcX1i0jBjYpBw UnDIyOrM6IveRp8DLs2VmXgs54nPw9cAGO+yDJS5uDxH38qh9wxQDRMJsMgRoMxoTyPiqwPw4g7x NCAwI6WEPg0YOWHkjDFnxP/ij/7h08AEykBLDVpE8JjdyUw2leQ8CCF42KHnWdAJTi45ks23wwQ/ yQmjEW14Jwga0YDCrO8bVAMb9bgW7GHEnX3xFAkdLkEGCNJtYxCAQSTgJmcAVg8OXhuhSAOAFTg6 l8R0IkiKmqWqTojnsHBNRBm/oDGzGWKKqCM6pirHMi5Fq6O3hxQVI2akPCCnAaxJvMznIkAAVSkW VFrPznipaExIJFNx+GOYjdkWIJHY50iJROwCVus1dvtrLFZLnIYed4c75DGhbRtEIgx9jxiCRk2Q F4EzppNzrsZIfuGZaEAKLGxDl6NypqSKkZnJA+ZEW68vsqcXckHzUFqTkOtrqnVnK0BNH2wMuZIy JqZFZBk3qkCC3Z0JkSNCJqQhYzgN6E8DkFk1UJJ8y1T2lv7eqoCKsilO1+SMrZS/go9zOZvKPqlY miw/Zdq2wef7z84jyRxru9EyfppxyUNZbW1XwMLZkcbTsyekElCRoeYX0rYqE4kodvwMBgfn3DDN h/ewWsNCchQAoOzHtm1xtd3g+nonZr+cMaQRKY0+gmxICiRkA1Wtk2rNvkuSvAwuMPtbP/OsD/X9 7OXddRWgfj+NqjoHD4VGTibXztU5dZqktNo+eyRHjJrFNSCnhLEfVAun4KOJgDp/i5OngA+p18MI KvAIvQlAZcJmj8ArfTTaJ+CCXYB0Z15rs2XU1bEMRkN94FQY8XGZzsJkL8zGvPxdANrbHMffdUxA 6Ft+f9s5X//htta0rhRJIUOQOHM2xx70/A70+og4JhBBTCEsWoukZtz4n/29v/80jQltbJBTAo/Z JfihH3A8HND3J3BKiJpVkLT2AGIAWfImlsRPrTKOCTOoGn1GkMgsp2WjfR3ENxkbs+ex1gdJoyQj Mg2GOwEJJ4ghoGs01wBCKVrm0qZNoKq5K2JUt1wIL4GDLKsEKUaWsmhFYohoGs0hocwmxog2tl4v hL3tloaaJJQveAHc6vkX14XcJ2eMY0ZKWRRCZKG5opGJmngraKIPAYyAJEkpCF3aapvSskWyEzon Yr4ZAq6uttjtr7HZbMBEOJ1O6IcBANBEIRZeNr7ayMRA5oQQGHCtRHnNsyOGilA42BNdKwiindFg XbgzIlcAjwGwaLkIWcAZmcTETogK0GDdG6HqMsGkQsurwX7N9NoJyXcPUY1P8tMJoZKQxjzidDri eDhhGHqknHTeBBQLJlbJ3yMl/FZTrn/fy49KQDhjPDRVGaOoZmsSSoCHrlmfmUTiTGTmEtEumULI AY15ppPCaNMekjiTaUP0GdULcBMezAF2Mtbzzpa/DcQAcGfWEKKCfGBzdYXb21vs9zu0TYthkNwY nNmlU9tzoh2U/VSJVgUQXPwbrqGr6Z6ZiC/Ng0/B15FcK7o1ocUXJeEZg5uDjtlnybAq+w8hSjI+ IozjiOHUI49iBmysLhOMFhd6LXNfJH8mSWiYo2gsg5pLIuvL0hBoczkAYwCSOtPHENGGIJmNCSAN 52dA60Vpe5XGRQ6IHBCy+t3B1jEK+AhZs1eW700QtZww3/R417x+V2YXvbmKlLJSEkSjHojQZga9 OiA9e4lw6BE151Tm7MU7c06SWPEf/eEfPl2tVs6QmET1RwBSL0y573sc744gENpFh27ROcBIGgYa Q0Rk8nz/BBTiUe+k6qhLis87900Ok1gJBM9LYc5hSoRJF6pQYjWRNA1i2whTJ2F0dXY+uW21mS4g WWGLGV7YiSWSxTzPRY0tTKCNLWJoXAqIoUHTNJJqnFBJu+fEYP7cSwdBQoPyKDk5zOHJM4uas6n+ 7RK055syFhcL4XGv+0rO0rmNOq4IEU3bYLsVX4yu63A8HnE4Hibhv6ahyUnQr6QSz6DKrFEnwjJH Q1LVPwgOhKAMTZharaImmNhTyyBlthigLKCC6iiX0ldnquqfcWYXt8fY2Oh3Z8t4NoZ2nd2FFfyw +fMQXCvR9z1OpyPu7g7glNDEiEXXebuiZrbMmib+6x1lbkuTawZjmpZLV8kRYHZaONHN6qDs4EI/ mzOfS5YFIsr/zlzqPa2Sbc7yMsxGBI4kAg+VWi+TsOuqDxOmCNMJSk+ymnZtfbZtg93uGvv9Ndbr NVIaMQwDTv1JNG1a1FAAR1kDRh0mGorq7/JetCz16rQ/zgBGOdV/f5uEe/Y7hcn19TPK+4X7VuN2 NoZKQ2B1Q4IInh6plhnZQMaQRFMexa8vkNSDymAPLTVmbcDSNFWiqq/ABVz/poKdlHNnr02i5RkC odWEiyAREhJb3SqhcZEbBAUWkYPf1zS0Bizc5ArTdEyBxTya60OOd2ks5ud9N4dSSmWYRqcaALEf kZ6/QvrqFWI/Iqq2P7E5/2efy9iO49N+6LFYLLFYLEp4JoQ4ByKJSDie5NUPol4nEmTKYkkQpHkW 5KcEtvIOBzwq4hJzlrO+6eAFR/2+BzRcNVkIJ1vSqUaZmqDmGEX1HGLwtjsqoVm7DDhVPbBNYQTP fspgjDlj0GyARIQQJVU4s9iFDVxYETHTHDjTJ4h0NsMc9UuazCqrC1VOaZQKs65BITAbQ5d5pOhb ySMHkTU01X1NZAzNlGHe2iLR6xxrdErKDETCerPG9c0em/UGY0o4nI4YdQysQFHOWdT+qlkqYRXG dKr1o1oF06vEAnHc98FsE6J9KHKsEPeiSXAC6jOoDAlTMwb0WVx9gpta2L4RNuEel/YYVtOcfsUz kwtXug4u98tVEa4QDCRLhtbT6YSh74HMiDGiaTpIXQyohq6su69jl70kLbuUrvNNip5s/HT0dB5U k6Nf8ORV+VDVwwSAIVotu1CcRAuBFy1FFnDB2c2AzlRU02GOfTAmJTeoOzShOd5uP4d93M1UGWPE crnCdrvFdnuF1WqFzCxzMY4icfsekruWlWmfp7SuZiRTE0kF5oBK2zGdy7Nxr55HXJtgZ/Nr92Gc tenS4caUGvVMHi5aG3NeN+227ZsAktD4TMCYkE49xlMPJDPbWr4KTREAePkFjzZyMEETjaUOYEXv qcyvOZAbndMq2mIyUUEkA+CAgAiiRigKS0ST0HIosFBtBRg0eelahZr7jK/Ru8f1vuN9fS6+q6Oi ZrBFEokQM4Nf32F89hL86g3aJOA+s6ViEPqT1NwVX37xxdNXr1/hyaNHuLq6cq95kSzVMzrJBstj Ag8Jd69fIw1Jkjd1ndiAybxJqw1iTZyh+UL77gEXEzrw4YNozM+OonoTpgCt9YDMaJpGMwrK4Ig3 etQcEYW5CcMR9Y8n5Kqmo267aAHq/rAOo+TEyNkAnI6Xp0zWRRmslHyQBW1OlmdPu9B3wFPrggkR Ot6ckfOINEo5d84jQohoG030FWQDtrHVtgTVuBQHyLPxNZBIwhgDhBiMKSOrxpGJsFgusN1uJY34 9gopZfT9CYfjEaTMwGrGkOA8mCeOOSUKWCgRN8ZsJpxcAa5IEwluUkEuEqWOJ1U+D3OyX7QOU/2N EGqqziqjXrMrPiPatfW1mnc7wwCGRax4EigTy63vssnTMOB4OOJ0OGAckyZ5CwJOY+NOntJcmrx/ 6DGRfO1d552rc1D3l3kSWmjOwWSb33cU3PyXoU6goWSxcH8nQP2kGJGLUznVgCUEnxvzOBGAELwg mj1wCiSmc0VEAvCVRhCJCVGbghjF7Pfo0UPs93sAwOl0dO3bmEbZg1A5m6vVQWX5zMdu3o7yuRrb ak7rMT9/FdB38bo6Z0d1cHXe2VHRANab1uYSXycheNVc83OIkFej75QYaRjQn3oMfQ/mUjEYJGYQ 863QAAUJi0TRJoCgDp3muwNPHSA+PGWNuIZWUwLEGKR4YGgQKYqAzBDHY6UhQj5ZtGaVloK0PTWw mI8LYFFx9++593XcvO/4rjUXvkeVVzdECP2I4dlLDF+9RDwOaJKAvqz/AC4pDJgQf//7P3y6Xizx 1//ab2O9XMqGgkhKsW0xckKIEV3XYRwTxkEiSfrjEXkYRUpoWyCao5ZtflvI8A1971jrQM1jneWn Dx3EagfnIlVae6RtQVQ3YxJ1jjoXOiEyyYUkfDUqmDBg4BJB2bUwpFqaK4uMtWOWPtnwTcpZy7ND mbsUC8sm0TtRVL8IJer1MJ6FQMHARRQ7PLIgzqg6kJyBPCANA5DHAgKZQUwCFmODEBugihYosmVh Mi7Zu6bBzjNsQBppIpszhoDVZoWb2wdYbVZIOeF4OiLlpISo3CJQA6jPRHAfCuEkla96cSx0EczU leZC6G65fn+yKAfY5p5qMqwNPsbVeIPZp8Hv57+LCcc0F5M2Mpd7EMrYTca26KjsRAqqtTFzjFLB gABkRn864XB3wOnUg1BqlHjkid3HV+Plw853R2kUZjE5DxY1VUbHAGANsESKg4cXlpboS30rZEjJ q62mQEihpBFn05AxIzAjgouPBaE8w8E+GbYEoCA0SLIyixabbtSp0GOHmENZ9n7UyCiL7FGw0bYd rq42uN7tsFwuMIwDjqej19jxdGO+SbmgTsjzC/MvT7/P32UyftV17yXlVu+lrzR5Xq3JqMGCHGHS RgOIE3Bu9JOCGzItQsRBljnSa9/zmDAMPYbTEWkYEViidcwFIlBwv5oISZFv+Ymsxk02584ayDDQ sEUJBgFGlckmhKgaa4mCDCSOvRJWnZBCxhhFQIIKMWaGsdBMkOhkinYF6gtiAQXTubs4L3/FwQWM TkL6Tocepy9fIL14jXYQDaJQWwMXANTUxADif/VP/unT7330ER7c3IKYJQFSFjXX3emIf/vT/xtf PH+GxdUGTdtqXHhGHkacDkccjydkZrRti9i2U8QMI20Vob2vM3Q/8fuwgaSyOcowFSYKqG1UQERO AjCEKZvEI20JyhSbRs0kigwyz0lm3VO4lG3SjnttGGMmq2dQ+YII9J4RGtWiNFofxaRZ4+vz/tnj YaGRyuyzOOkGMBoiqTcyJqRxAKsXdxOCSnyaoEg9uKXLedImD0MFFASZ5A0HFKbxUUwAkICo2DbY 7rbYXG3QLToMacTxeAQjS0VKEIgbEEdxbGQNg7VN7X4Q5myqfTZPeAUXNh7Cd0zqKnb3wm5rfZCZ IKyv1Vyg6B9IQfRco+Maponaqjh4Eipggarx07uAmRyUcJb07TGII5sxL/NtAgN93+NwOCKPCSEG tItWNDXVPvDHXdhKplmr+zeXuli1EdMIpsKgbJCICrCwhxrDJy4OmOZ0bVEkiQRcjLo/Sir9UiHY JEa5rlRrtTgTyvrS62yXumChIeh1xk7bLXV/5drs6xhgLfUuf9v9Q4xYrVa43u+w3V4hNgFpHDH2 kgq5wmBlXO8hcnNQcR/AmH55+V6X7m2nn/11YS7JiKBdS+V8+S7MrlH/hjrBmYWwG2F3wYhVADVA o7So78HjCB4TpMq1OVZCakfpPjbzWgb539ZES6sWADQk4MJ0GBzEyTSE6G00p+i2iWgjoaEE8IBE I1JMCixYgUVA4Op+ENDC1YjaufeN/9vm5kOP7xJcTFJIEBAyI44ZdHfC8OVz5Fd3aDOALNph0XDo SJhTJ2fEf/ZH/+jp1WqDCAISo/FBBP7y81/if/xf/mf825//GX7561/h+ZvXiIsWq80G4ziCGRhH qY+QTgO60KANjTtM1qrkkkrXVIzm8FgILqEUHpsv+vcalEofTWU1u6qsJrK+Z2w41aadc9KIEm1r pcVoYkTbNEXOZIvNxmSjm+p2ovauNqlI9nJGZsaQJTeGhK3Kgi/IUf1AdFxNMcgWimibuhqHDFKH VWuXyBKBLOmW1v9ILAmCxhGcsjhHhQCKjYQ9qm2yiUFNZBK/TKrmrqMHWJmYEw5g6vFOQmyYpD/r zQb7/R5X2y2IgOMw4DQM4AwERH0V50rROmmwoknK1gYFZnZ/mbMCiCx7H1f3g8+QBbHpmLOAJUv9 PfG1ICio0DVkgKFaC7Xawwgp0WR5lPN8GxtoNOCk2g9FT4GqYFjVMgnzUtNZZoyDZPg83N2BWbRB bdMKMWV5kr1bXwDbMxZCKSGVcZY6elIjBtPOTACGDm1tFrI4ncgWaVQ0HRaOyqH4TGSg+GewjV2l FTHgYvTD5xIImWbtc2IAA47BHP203UWJxH6u0Cab2XI7A5VQoGJEZLFYYLfbYrfbYbfbgjhIhESv BQbrjKPqgxNIfADgAo22J2enmVR6cPHwsSGfmXNtpq/F+roCDibP0O94cm6tYamvIz/f3t0Hy4GF HSX/g+W1YB3nQOqUmRncjxgPR+S+B6WMhoEmiMlWssBCU1Bbg4oW2XdpIFVUSBtkM+v+9hBpA0Pi 5LmIjHUEdm3AzarDsiVwHoFxAKWsPmcRojtrAGoAyF5RcQwBDKsrBF0/b+Nf7+Jttdmk5oX3XXcJ nH4o/5xeq36IUC3QkDA+e4XTl88QTiMaFj6SjXapcCnllETAiv/4D/7u06BZIiXfhTCHzIxnL1/g //jp/4Vfv/gKP//8M/z8s8/wi1/+JZ69eIHbhw9xtdkg9QPGU4/xcASfRhADzXKB2LbOAOtCM64i LuKhEzZY+NrXBGWGhDHbULUcVm84RgEZrFJiSbSkyVtIF49644szUJzeAxqi6o6UtvarrasPcolU PycwxiT5MBIndewMJQdEKMQgaMIgXy+5qKLkjSV+3E4IxdOXnAyrxkFHhVnCZTllkAGWplFJgBED 3OmVwappYXe0KkSCnOkGk1Snk+NDYUmN2sUC290W2+sdmkWH0yAZYs1GK/Jj8nkCKbAgk45k01st Fih4aKJKKKYI5+DETvquEgjbiECJuUlTCcjJmepE40bVFwaKnfqak6g7jhhf82OiuJj+NCXGRX5Q wmXgg1xcm7A/BjhlnE5HvLl7Dc4ZbdepWnpKpCrcPVVmOEHyG0+ARXDGO+vDHGDgfibnA0ZTDYU7 wsHs3yoBodyAFWWxJXLSOSU3l1UgZPY424IgSIp88xvydanGj1xMP7UG0PoY1PnbNXX6W9u2Ugxt t8PN/gYxRBwOd+jHE6QXqoWJwYUB153UY2xZkOv5sjm6xEAA73Pd6RoElHGZfzedIz9qocDvdd4e +2vC+CpaV2tBLGlWGW8DQ+IX1jAQEwNjBvcDcj8AythDEyU3RhOKk7DykUhmjhFQIz5vkHXio0Mg WH4fCYONRIic0CJhHYDbRcST9RIfb9e4WS2xDAFNytKeXOVpoSiaRVYnVSIEYjAniRIMStvPR/WD j2/LbPKh9ymJ0oSftQyEwwnHL59hfPEKUR05NS4SMPqkWaNN+I7/6R/83aclmaHZUBgjMrrlAo8/ eoKbm1uEEPD69Wt89tlneH044Pd/73exWa+RR7GV0ZgxvDngeDrhkJKDhq7rkFL2abYkSbIvDdG6 XFYYyTcY1Pm0OoGvfqi/82JNLNCLNVEYQZB1Expx7tRL2kbzUtjGyrnUIYB7CHiSKo97tofaZlBa LIKoZPXMViMFJBVqPWGMbJ4YJL1tXazHok0CMShU9m7n54VZAZh6n7NqZ9QRZxwGmEe0ZbkrzRYT EVdynYAQwLKrxkBul6xVwDa3hVlI+2LTYLO9wm5/jXa5QIgBhzevJNMoj+J13sDNMOKwqPd0p6ks 5bJZiFEes2ggcgBz0HDFUCSrbHkIAEICaU4NQkYg8dWIVPm+GJHMJunDr6/Xk0xpiWQRoGvsCQoO 5P5eawQ8qePmZq9aba/nWaiqffLPtn51HlJK6E8nHA8npEFS+S+6Do2lbSdy016MAiRzFlVmCMHn rRT7MqZC1fqZgQdtC4E8bbCrvkFuJ59HjpR1SA7MZf3LP/HHEGdx81tyyZtl/QbfyLJHVJ6qmKO1 V/95CHZR46ttzzVbpjWRIzjDrA9GkZgNOLRdi6vNFXa7a2y3V5rh84h+6HUdcAn7NgbpghecJl40 PTkDx1lb6nm6V1qu5kpAwPnvtcBVaEx9/zBri9yxlGoH3F9LvzPfKKN59mjTyFkESMNq288ZPCaM fS/+YcwKyqK2ybZgieqxkFegAhcVvbV9HAlowOg4Y8UZu0h42DV4slrg8bLDbdvipltg3y5w1XTo 1Ll6TAkpC7uVMdKIJNa8GepF6jysiJhf6/g6wGKiYfwmTFRuIGuTgWZkjC9e4/jFM/DhKCAQrJlM le/p3nfyBUb8x3/7P3qqLZM3MMZA4EigGHCz3+Pxo4f4waffw8ePHmN3tcXHH32MH33/B1ItUBlT FxpEBLx4/Rp344hEhJSSRl1ET/zDeUqwmK1KKAsDCEXq+9aOakFz9W4/Wf9JR4YVhbmqmqnSHJBn BSzMp9wDXNTKfu+KMMMIBwqqt83KpEXCtJz7aBnsdANRKOo8UfF+Sm44AAAgAElEQVSK/RAG1ogr r3t5XJgsMiWalZwnG5wUeUp++DElgJMUGNL2BoLnAmnc/0RVuJk9G2iM0TNyW39tzAsvlCgdkPie MBEWqxW2ux0ePLxFEwAKGYfhiMSSNMqHV01BKQ1gHhEDIwYGMIKQEELGdr1A2xACZXDukVMP5hHM I8AjOI8gYoQAhCBmB0ukpfGchfhSPXrywSI7fGqrZSYLiFzTYMTc+j1d2eaHAb+f3wLz7+TvUkmF J+dWEAYxBPHDuDugP5wwDuIDEEj9h8xvBoCXK6/CZUt8f+ncfSrXOQMkkDu++doLUxs5K+F346E6 eMqrDHlCKYqWIGpx09TYPzO1mNTqFR0n7a3mxwAdoRQU1DolBnYKqCqMFNXcU3Vf63NWrWdQwLJc LnBzu8f+do/FcoHMGf3Yo3dTiTAna1Ntqrmooag1BhXNuHTOfI7Oj4ogXpzPmTnK56Se52oeqMyF MKUCxqSdBY0awLBWFS8GXTdQmkUMThK2OvQ98iA1m8RBPXo+DNtApTaUTZcAyKB8JxAQOaPNGcuc sCXg0aLDR6sVHq8WuGkiNsxYcsYiEzaxxa5b4WqxwqJtEEEllQHMsTtr2QYxFYvzKGtStG8GLj70 OKtB8w3AhZt0oCb+04jjl89x/Oo5Qj+ixHppoVBmcdBmFSAAABnxH/7t//BpRaeQCBhIlNEmRS9i h+1mg4f7PT559ASfPv4Im9VK1d8SfhJixFfPnuF/+zf/Bn/22WcYUsIwjMicsb3aIISAlEYEgoY3 KrM0klij4lniqm9t4KzjPgD+NWyjuETLACybm5kLgtUIYXV4lM9NDAq04NKHlXT3nakL3j6yqpUm jVDim5kx5oQhJzXTQG2JhaC405S2Q/CBghPtX8mALP4KLuBqyJdtfjNpGEMLAPLQY+xPQE6IkVyV CGLx6IZIC+b5DdUiiHRszEPnz4iYJchR04+BS4mYiWi6FsvVAg8e3mC734JixKE/4Xg6QSw+wrRD ZARKiCEj0gjmA9qYcLNf4pMnW/z+33iCjz5e4Pa2xfU1YbNmdF0GqAenE8CjAgmAMgEcEahBRItA jQ5xVqCZJdGOFJ9AKSlf+qdfOcM3sDBfpecGBR0fn5gCWoAS1VL9Kgmpqs8l7h6ukcopCchl+ft4 OODN69cYU1IH5VbGP0iF1VwxU5cMa4n5wjH/vmZ+5mdBNaAgGTUzo8mCI2+3keKgSNT8MXLQF2QK EkvIoD2LqOyLbMACJk8buEOZL92b1aKE1cwgzbsiDDMUOlH+876ahEYqTc9NG5Jci7DdbXH74Abb 3RZNI2nb+/6kmsbi22IapRDM96eAoQJtZnMgjbmgXaBJv/2cCdioNA80vaeP63zsqnYUAYlgmUkn zyAzPhaXR1XCVoCiaC+Cq0yUHhChIQEcPCSkYw/0I7gfhZlp3grxWwGQk+f2iErPiDQtPBiRRyyR cRUIt22DJ8sFniyXeNS12AZCl7Kq+y0iidCFiHXTYrdYYrdYYhEieByQUo/MCTmwCuGqRcvi+Ong 6q/Q8S6t1uxsGUcQwpjAb444/Por9C9fIagVwkE8i3LBeI35TAFA/E/+1n/wtMolItIBQbJVhoD+ JKW8SZ0914sllrFFZAJp4au2aTHkEf/nT3+K/+l//1/xs7/4Bf7sF3+Bz/7yMzx//hycRiwXHRZd i7ZtJaMdKjOIqSZ9GfO8r2cD9SGH2+lm77avavuw5IgwIgUhRpwrjUthkhSCI+kYSyrbpHks2IqR aSdsu4qm3LQitYmBXfqShCyCyFNOSFkBjWXXrJiAEUXrG3sCubKxXT0NQByc4IzMNQvahjYAGMXR M6ckamrbuK49kQRkTYieV0MAGUMKTEGZS01wyhibc1tNIFJKSJywWC6w3m6x3e/RLVYYRokoySlJ +vR0AmFApAExnLDqRjx6uMLv/PbH+P3f+xgff7rAw0cRjx+v8OjRGg8frrHbd9isGqyXEctlg0Xb oAnqK6MRGYa+iErYIVDZdEmZORUYSobK1FwySaJVLd2y5DSa5F1ruJLgi5MjK/isHkHKRLmsMGjm SmfwScxdfd/j7u4g8xcj2lZy1NROsxWNf+cx3YdFOvXYHDdllJEou6D8XWsrTMplQBw9AZh2RRxy FYD7oBpDEjWt7Gd2pmb3nSdFc+AAqgivAowQEbSwWtbke9ZfE4BMqUmhyr0AAx5ZmF8je7JpWuz3 17i+vsZqtRJnz2HAOAwCAl0zZy2yVk41Fjxp67k2Yw4i7mMmcwBiDyzfcz202pJybhkH+Tug0L6a 3ng0m/rEWEipm78md5b2Q01GBEZEkNwYGQhjAkatU9IPssaDVMiNGqFmwMJq6xAzYma0nLFCxr6J eLRs8WS9xMOuxY4IKzCalEGZ9R5mImNQzmiYsQoB267DpmuwaANiADISRh6RNBkhEYFy0CgoHcHf EL54W4VUO+7TQt5zMogkehCnHv3zlzh++Rz5cJRoGM3/AS4RMoFN+CFjPoj/8d/6g6cW1mWbMzKA YfRwMUtJDbCo8nIJaUSSmKsxJ5zGEaeU8ObNAX/+i7/A57/6HL/4xc/x+S9/id1uh8VCMgk2bTOB D0ZOPF740gaqPr8rAYk7E1aUvZainYgCmuPBNgn7ZgAq4gxo5UQLSSsRLQCKmUJVrFIuPRTp3dqq XNzUn0F3MCljIj0l6SazmG4BF1lVfuTEhFQHaOaZ0Cgj5Krwjw2ASUgywzom8n02lbhGkwQWT+0Q xKY4DAMGtRmHKOXmJTNkoyGS5IQl5QwOQc0y5OrqeqNJOLNxyPJOIFAjHt2JIharNfY3D3C9uwYh oO9PSMMRAQMCTlgtMp48WuN3fvsxfvd3P8b3P73GdktYLEd0XUbbEZaLgKvNEvv9FR493OOjj2/x 4MEVHtxusN122GwC1iug7QaEcATjpL5JpNoS870oXiu+Ds80EQoWyeCiLT+anCLfCRqr63PQhSvY 12/xh8r+iwFUVGPJ7oNA9X1YKhZbnZdhGIQoatZCq5eSLbX6PVqWdx0GCjxlt61RQBMh0cTh1yKK BCAUAMVksZxlbRHEdOFZHImRgzqVKaAI9j5r1yUzCVCeC0CdSiXnjGjJSp9qKX0+Ngyu1oJqtzR2 VhxEM2LTYLVa4mp7he2V0MKcEsZhFM2YtjFasi/CZfpnc3sJNNhrBirmJpRyZvWpBhszjY+vJTLQ ECogo6t10tay+gw4WgiymUAmq5PUL02jvUSbodIwW0ReUH8cRh5Eg4GUAfPn0zBYVnAQENACWIeA 6ybiQdfg0bLF42WHfROxQkLDIyiLD5T4bEQwBCBIH1T4QAaFjLYNuFp12K4WWMaImDOoT4p0IzIF jKiqtf4GjvcBFsB7AIrZuUTCR9LdAXdfPEP//BWaJDwoG69mDdXNUuOl9qsiAPHHf/PvPK1VGYFZ HDaS1ghRZgINbcxJMgKy6iiF2WQgBKw2G3zy6ad48uRjPLi9BeeMF8+eo4sN/sbv/R5yGjH0vdrA AhaLJZquA0MczFgXZfCgQ/LvfIP4nje0ez6PrgOpwMWlgSagbGQHJDRlflRtOZbFllPxVDZ0b/bG ECJi14mqzK+pNqtpE6iybcvdYZI+a0ieia+k451yktTZlUe7mcxBBMSgefTN2cjUrTIqBnQ8LFbb b42QTaZZPdUcY863OTOGcSwgw/xNABAFtFrUDkQYMsDq1OXZ7fSo1b9uqoGpiKV9iSwctkHTtNhd 7fDw5gbrZYvIAyKOePRghd/64QP8td96jB/+YI9HD1dYLxMonEBIPrnWp0UrGov1usX+eo39foXb B2s8fnyFJ0+2ePh4jevriN1ugdX6Sut2DEjphJxOyGpKIbWvWqhqtRJ1fs32LOvQQxwNRFTEgKv/ PYwNcC3EpcXrei6dL1vDik/VX6i6Ss2WzjqYMQwjjnd3OBwOyInRxgZt08DMIlAmX4CfLZdKUzPb Uw6CqBAZ5x0wB8+ipagZWX2dXMtOoEQCFaoAGGAnd4IuoXAVALDGVA2YMN2KERp9sU3p/xhei8eq Ftv8sQkjZubT/pcqm+TngUr4s5mkttstHjx4gOvdDkTA6SR+MT6X9VhX7XYG8RY+MTeR+PfTs86/ 8XOn5rz5mHk4cf0vBB/7MoplzN38odea3DFpjwIyE+cC2KOTzPRVyrEDUH+Mse+lqCYR2iaqbwWj y4w1EW6XHT5er/F4tcRt22AXAxbMiDkZRYQ59DLEAVw0gQr+A6RQme79LhDWXYvrxRK7doklRWDI QhuJkKKtlXOz6Ldx1E7W7wss7Pz3P7mUvU9vDnj966+QX9+hAwm4YxkXYqvzIzTL6gABGmn49//m 33lapCnZ3QIiLaRRVIO+9PRvl3aDec8KulyulrjZ3+D7n3yCT598hO9/9DF+50e/hU8fP5GIksMJ 43FAGjNibPGzP/85hsTY3T5As1ggjQk0ZjRkURBZ621IyCbFqHnMA1hDV4Xgsdv0MmzDF32rgYDC yJWIG2PWBW4ovI6vsKqFDkY0msQYdwzRfQ+slHYT/x/23qxJluTK7/sd98jM2pd7by9ooLsBDAbL LOSQI4I06YGiqA+GjyEzPckk09D0oi9AM3Le9ECTUSI1mMEAaHJIamZ6uWtVLhHh7no457h7ZNXt RgNczETGtbxVlRkZ4XHc/Zz/2QNRdLEXcm2J7Td1S4iDhFKnxEHRkkm6KTjnxJQ0OyLbg2nKqlSm O3jwZ1QLgkamQ22rjmndJVdQFUS6ctrWU6EKMQ1UImVymrR4Ctnat2vArjKYAHGFyAqRQd1CxTIx pIDk1k2yBAzG1HQu/K5W3ZNckJzYDJHLsw03Fxue3qw5P0l89K1rPvzwmneenXB6lokyghyQ4FU9 jMm6EDG9HxJDhGEQNieB87OBy4s1Vzcbntye8s67Nzx9cs2T23OuLgJnJ7BZF1ZDQsoI6WDt7M33 KzCINu3DNl8FF7Y+SwV5aiGoLCFQ6QJakEawObECPcq0u7VrQbRivhEXwFStuuoWvlhxV0PwNZ8g j4npMDHvJ4p1qlwPA3GI1Q2j6bsenS91R/SgCmgppUiju6/cBZhoQYtLu0q7WMvG6QWgC/8FHLN1 LdZxNTBJYK7XhlqZUbprCPU5nC49/m201rMUXAwWfCn0rkw6EKgtAfT6fZv4xdObDF2tBjanGy4v zrm8umC9WTHNE4fDjpRTRyO9bl0/SBWEPgvOuvqsFerYaH9USvp16F5Sv9tA5BFQoT1rvVb/XX9J m9ne/VKtQz4VFSw5pTzTTtPfNfnDXBXWvbRamIO6VkpKMM2EOTGkzHrOnCa4lMKTdeC9sw3vnZ1y u1lxGSOnImqRrWA71H1WV7U0zu/ruj6Pa+oI6xg4X624Xp9wGgKS1aU7lYlcUtcColm0/LmXllxX Pho9OKLzkuaPW60W59DJNr+LPLRFGgUe7EXBLE1zYv/iNXefPUcOE0MuGnPh1v7smWxoCq5niBut 4n/zO3/zJw8eyAjuQe/Lh2vamDchcpkkpqUHgZP1mpuLS969fcI7VzesRBvXlCkxT4nd/Y4/+dM/ 43/9o3/EP/+X/w+v7+/JCFeXV1ycnGgGAkqhZJUsh2GtGkIYzGTdgii1Z4SeJ8F65VVGsTTzPZy+ xoBcH3WXQVv8HaM0BuMBrxWsdBvTXRhDbL07NLpYB1yZt5WnLUUqWGtmyY492M53PjhTSFZpUK1y iphDiDWjQmLTrhrPMeboY+xoIbTrlxp2rd9yi0vA6oGkmezWLRNsqmFEMytHPbdkSkmG/Kk1TcT9 ouL0zJWG2egbSmYlmZUkVrJnHfecbxJXF4nbm8jFGazXM8hEIZnWF1odhCZN2q+i84AkRHJ9xaGw WQdOTgcuzgZurgaePj3hvfev+Mb7N7z79JKb61Our085Ox04OYlEUbCV0wjFwYaZESo0oAohb4nu S06zR0ptKFeFoLlkcnH6uTXC/dVSN3VTDJxJNbPFMhS0VKYQMtb9MZCmmf1ux3gYdV0EQVYDmHvN 3S5SN3/vAqAyYRVw1d5YrRR9PEVdzQ808O7vxxjs8emVKTVLn4ILIbkiYP+5ub1noceZBb7ufWjV qoFr6+oWc3Ak3WjKgh6lBU0vsgU6xi1Yy+/MajVwcXnBze0Nl5dnxBg17ijNap3shLCX1i6lu56D gRrsrXywj79xIe807wV+fxxbOR4DE/6p87j6e0c3rydTaVvBnR1d8TGpsRLU74TuNA2Yj+0eNibn XasQWIkQU4LdnmGcuR4GPrg+54ObM969OOV6s+KEwlBjA0yQt1FSK/jWgGBbE7Z+xPZbrHTSeIw1 wsUwcLZecXqyJm4ih5I4TBO5mIIXfC2VxfP3IMvp+Jhk+nWOxVW6JdiL8qpoLNZCW9mxFMp+ZPfF S/YvXzHMiZC05Hd0YGHgj1BagGcnK+J//aO/8ZN66U6reGhwae86YTxXuz/DhW9OqcZmRAm1QJeg FolxmviTn/6Uf/LHf8yff/Kv+Pmf/5yf/smf8u/+7V8wjRO3tzfmD4466KIoK88zqzAwz4lVjEhQ X7LiCWnR3g4upI74K6bDSU5Flv27cnS2+5xSzswp1fLAMYTaLVZAy8uuBi0Cg3dZbDzUN2l1nYjU yTl+VXRrCzznpAW4StJxxaj54LlnSoNaGGIrppStIVNbz86YpAoKoAYnIcfjUYGY5ok0mxVDsC6z Wl55iIFhZe4Z8DIYaL0J5y+qWSsoFaQMNTh0JbAJiRUHYrkjzC+Q9JyQX3B+MrIeJgITMNv0Dmot kei27qNZe+wwSWDaiubiZ4aYGVaJ9QmcnQ9cXm64vjnjybNL3n3vmnfeueLZO5fc3J5xeb3m4jyy 2RSC7CnlUDN8AuYCqlYFqcKh1DUaCAwgESk69poUVNUCodSXWexK6XZjsmJjqjU5OPHPa00F+17A K5nqepzmid1uz2G/5zCNxrxF+8z0ANPWnttFlD/7irBzuj0i/S9HU1HjnqoG0E4qx9fo/vbvNjdp u38N8kSZnTfA6kVJoFixH+dX0o3a3Dm2+Uu7fBPKVZiHSl1NX8bmvI28Wnhc6HcP00B/YbPZcHNz w/XNNecX51DgcBiZphEJwhCtkVrWQFFCJ6ygBZgjeMxPi1sL9fl798aD3xd07megAZSHrpojd4hP CVLHUEGNfyd09zagIFhDM/uOVx1W97ROrBYyNJoWtW4EKUQyQ4CL0xPefXrLN99/h2+8c8vtxRln 64FBROMqSoO+3Yhw/qZvuUXO1pStS8Fp1da0iLv6hGG1YXNxQTg/YwzCIaUWR9NZhx8DcE44H9Wy vsqXH2+zYLwNXNRfy9E5dfL0p2aJFA6v77h//px0vyPOuQZuUpbrr/IhVyQM4C/AxYOBPRh0d0Gz Uiz8PiLN95uUqIOnmybbgCYYQ4ys1isuLy8YVitefP4Fn/ziE37x81/y4uVz/u7f+zFn52cIQpon ViGaHBB1qQQvSa0BjxKsZgKFFovcaxTdZnwwKbL8KY++W38WEw2I/p6TVfZ0y40J8iha8KqaCi3D om+h7WZg3W9ecfCIOdejaYb+dNlKsOZSSAKUwioM9X7avMeLzwQraa5HztnkvdQWyZoN0YKipGcC Fdd4AFQxC8YI86zFcYMaUaJkAxtW48SEvrvVPCyvIl3RKO9I4UQKJ0wMZUuYXsP8BTI9R/JLYn5D lANSRi3RWwIi2m+xyGAm6dIYxtc+MjADE5mZwgwkYkwMa9icRi4u11zebLi9PefZOxc8e+ecm9sN F5eRi4s1682azWYgBhX4pUxYKSisODveNVGLQA2EEsHARSmz0cYCajo7qru3lOe6RLPoAzEE54Uk 6K0X3SoKQYtmeXptCGQU8G+3W8bDnlAK62HNYLEYGixnwKJWyrT14pYZv8HR/jl6+8E7NevG3jsG KF/Gkx67biqWCh6agwhoa7kTwD2gqdlkTjc3Q3SDKBbgKx6ojAss4wkl1I3yqAnb+JAGS6v10+te nJ2ecXt7y9npKSFEba0wHphNGaixYA5whBobVgvEIfVfT5nHFMYFfRegw//u3sf406MAg6P3pJ3b 0z44gKABCVoch1vlghWoCoSagebgYjCAEQIMAVaDcHtzyUcffcC3P/6Qd999wtnpmhjNSJDNUlsp 88iaEdo8swQX1Z1QieeAUT/M2crWrzeMq8hWYCqCZ5ykOYFlDwVbXHVFLtb8EcD7DQDGY+DieC9V vcWkWQVSoinscpi5f/6Cuy9eIOPEkDIDqOHA0xEtNqqBsv5eQvx7P/z9nywJ9uUbWTdhWfCrB3Uo vMuoaaE5e8U86kQWCqenp7z//vt8/OGHfPfjb3Nzeck8T3z44Tf5h//9PyTGQJonzTCJQ/Xvr9cb VsOa7X4PAnFQK4VXGPQV0dO93ywPp+FXAxftd9P66eBgQXvZ52SL2QWwf6i52atBO1eCGCBpNFHa l2op8awV6e6r9LZn85LcWIXPnJjnxGCgq7lddLMHUf9lqKl9NndS1GKBB6EZiBFnSU2/a5pDYBCQ khRgzBOkGfLEKkbIiVCyMQGxuAwhMlA0SkpTQNFCXTFkgsys8p51umeYXyPjS2R8QUgviemOlewZ mCGZCwKtT1EsCDGLr7PSMduvexQKll4W3BwOWBVPb+EeQmYYCuu1cHYaubxe8/TpOe+9f8N7717z zrsX3NysubgUzs5gtUmEOBFkpuQDYp03F+KgRLBspKqJ4hHryYRKJhcFPKWiUI10JwdKGVTMl5bV 4ExyuW11EVWzfrBw1ZSYdgfyODJPkzZDC4FhvYIYNPUuBnXfmYVE6wt4EHHL+jq2AD7kcDTLBU0w uaXyAbh423QebeMGLLTLcCluMdKTmxtB+UQ1qtV7GKGOXB4+qGAxTsrj+kJbbvmUxVgfG7b3dIkx amyUAZ71as35+TlXV1ecXZwhQZimUYVUd6HKX+s/FcZBvMKqLP51T0/d2/1rQcYlQds5zb30IB2e 3lpid+yEjuDuj85CQfcMQv07EsxtZ52aB7PI4oGhmZPNwJPbKz54/10+/ugbfOP9Z1xfnrJZBYLk GnBdga8o7yq1SWX/iC7s+3UYmjXM1wM1nBoQ7fcUBsqw4kDg9TzxZp6YS2kWY1Ewi4FIHuzDfl6a nPo6x6N8Tqp+0ebgyGKxsPZVC5MQUyZvNZBz/+oNQ9Ikj2g0cBduqcXWzPLbWS4QiH/3h7//k+Oh HSt9vVbhw1qiuXae9+iItmiylcau6W7SNC4RFbbnp2e88/QZH37jA77z0cf84Ae/zdPbJ+pXi56F EPj008/4J3/8x+z2e84vLlhvNpYma7X8StHy0OafrubCCiyOJ+Ixh8eDHw+ZgzElBzBiwlowgFFr QyjDiJ4/b4yodv2LgzGWTEpWRlWaP6wuuWPu6s8QsIZPOh+pFEpSl1QBWplxfbW28lItCgikkqz1 uQpUOVqVDpHUR2dj9M2LVs0LOUNOTONBYwjm2fym/vxClIEhrhQUMBAwCwcTQfaEcs+QXjOMLwjj CyS9IKTXrNgxcGAo6n7RvPmBEFZAqGVoS5jR4NHlZv26h35Xhaf3uHCGrFYn65gYFGRIyAwDrNeB k5PI2Vnk8mrgyZNT3n3vgnffveLZO+c8uT3j9uaUi4s1p6cDQ8xImSllQquLGtPD409882bdyGSK BX6yMLMHpEQFbdmBYcu48PXh68ddeCIaz5SSzb25L2Mp5MPEfrtjt9vp50GIw6BuSmeAXXCjV5t0 BrqonfIV9H7MxnRshv+VLoQKdq0R0+Kn+lR7reej+6HGlQEtpFroXRhLXaR/09eDWQXDkpdUyy4+ h+1ZvVaM/121epR/DhbweX1zxbNnTzg9OWWaJg6HA800rKP2WIymDPegoLc0ND74GB0fgI0j+ler j0MP6eK5jkHK4jtHMEd6C4VngDSwFEUL8Ykpa1H6BpKF9RC4ujrnG++/y8cffsDHH37Ae2atCMH4 gwu8DjMYdYGykAmL1Vf5vme6uQXrCHhJC/QNwwpWa3al8HI88GYambM+zzAMNYvO90upe0Y6Rf7X 51W/yvEYsPCjj+Wr5x5mxhev2X7xknm7ZyiaObJok7HIJV/exx6G+OPv/95P3HDaazX9eybn2sD8 zZqk3n6UUqwgiX6hbTCd0GI3RqRVvgRImZNhxc3lNWebE+5fvWKQwHq9Jg4D4zzzf/yzf8b/8D/+ T/xf//JP+ORff8LusGezWbNZrzk73ZAO47L09oKS/aZ7y0aq32nP9Bhvq89RhY6hbnzfJxWwXhK7 34gWNBejlmKOFlPiVRpd86hMwMZTup8ibeG3+WmM0ksRq47dGH0DONqjY7BgqYrMRUAauPDndT9y jeg2X2gxc2y0DqpYoOc0jZANXMSWghZVJ2GQFesQiSQGRkJ6g6TXZqF4znp+ScyvkbJlJSMDiVAS IYu1YR+AiBS33CQkJCTMhFCgpi7+GkcxKwARRFuoBfHf3d0glTlgwaiVIKUQI8Qhs4rCehM4Oxu4 vFxxc3PK02fnPLk95enTc54+OeXmds3VVWRzkhC5J6V7zUQiQ9HOGs58PWDadciqTVWmZTn5zdjZ aV+mQYHWp+nWHF4joEDJhUE3LmlOTOPIfn9gHA8ADB4DhVVCNP9q6zJamqWk3zjd6wGDe2ClONK4 ewH52JQd/d6ApQame0fNJBoIPYdipcR9nbdn0NiuJSNu2SX28ht2xfTEXYgWkO0xL5V3dnu5H2vf DVf5Sqka4Xq94vzigpvrG66urohD4HA4aCA56mJ0N0J79rfzt2PKyoO/H77c2tDSQhtocYVK71mn ajkGv7b9rimmHbAQWYALtZ4rvVfR6stkbUdwcX7C+++/w8cffYuPPvom7737lLOzjWWaJZ0nWe7F akXuxllXlhwJ9Mr2DYrYSf6sysO1llGMA3ggcYhsc+bleGpHu/8AACAASURBVGCXrC8U1HRmBxm6 x7KJjR44L2XTr3Mcf+8Y3PcAY7GJuoVe0Eyasj2w/+w5+xevYZq1BLjJdeUX1Ho0i+sfXTr++Pu/ 95Pjj2oPhKMBP3hsn5yOiwhYlQqDKA4kdM1YwRsLwrRI2mjMLICayufMfJiYDxNpniEE4nrFn/38 5/zzf/Ev+MUnn/Czn/2SP/vzn/OnP/0Zd29e8lvf+S6b1crMXqECm7dPxCOTaEygPPbZo8/fbdJi 9lVRTR4KJfVN0AyAxGCFcsyEH4K1FbYFXzzNt2NCzrg7jQAwczldHr6hbAnai8H6k5RCM/E7YzDW F0JQs6P53VNKOot1WltBospwiqUfe+psodJdTNPIeSZNGvApqHlziNpGPUphkJmVjEi6g+k5ZX5B mF8wlDtW5Z5Q9kRmjd8ALa1LJIYVmsbqaFstB4hq9aUUQmkdUb/+4S6FaDMbFFgU03SLPreT2wGh N09yRgogRQGCWKny1aqwXsPZWeTiYuDqZsXtkxOePTOQcR25ul5zcXnC6cmKECfIB3LW3iji5l5f a8483Yrkc1ZSnd8a9mlpsS7IPJXa3WS5FEo24WGKSbCiRvM8cdjvmcaReZxYDSvWMTKEQZ+1y+zS dXoELo6O/v1FDZhHX7L4nuDBqr1AXl5bfI4kIBLJota9LJCkMJFJ+uUKuANiDfdYBFQ3JFFNAzx8 Mo+zCFZtmAYocBbYMgQcfPj4xWIplB/YvSylHWC1WnFxecnV5QUnpyeklLW1QppVILgbE+MFvJ33 OTr6cpp3L5Hlz44GDy0UjbfWTs7+shFW4S4dWKHRS0GMNWGUQgiFzWbg9sk13/rwAz7+9rd4/713 NLtmEKQ6tbQnkBcm9Omq2T0O4JwKC7nVkaZSsilrfn7wlHsHlHFglsAeuJsTr8eJfc5qd+x6U4lI bTbZNwpsPKTdvHcNft3jAVCSLivk2EDjPqbuO5pqC/lux/avPme+uzdgYaum5Mpneouc+OVoqeYA 8cff/92fwHIxSj86lAKFtqGrKbEckaBuphZRXrrzofmwijFl5XfFAJFqQoMRI00zh8Oe3X5PKXDz 5Jbf//3f5YMPPkBC4N/8m3/Lz3/+CZu18N/+/b/PamVWAKSmAMU4kFOuC8ZTNUtWwe59TnLWehpV 86LUXhZDDDZeUQIfZU8oM6CKO28tjwldrOOkkjgTJS5oFa2s92qIdQzaS8FriegiEAc/HhQmmNbk AAcrZKI1SjKazZKsfLnNni5wE4xFsBLcQ92Ibv4qlmYbaFanXDzgU5Aw2HNES0VSxqrKbYGkIDJZ MG+UwmrIhLwnTy9J+8+Y95/C/JyY3jCkLTEdCGjcSjCg5GDCfby5qBusWBopYqMsAUqs4Nbp+7W0 gcrp7BlYvtrc6yKtKXd2vlo0IpKjCisxi4+4Dj0TYiaGwhALq7VwuomcX6y4uTmrAaLvvHPC06dr bp+subpec3oqxJgQTULGi5xRIJRIUOiFADmPhJDJeUYQkhXoKjlXzbDtdyFnT/8NNSvPmSIuzFPW /bjbM+9HsLL/0QWjUF0Rb0UVPY2dJ7zl9N4oaqsb6jrsTdu+N9orlKABstCapAWz4ohUpTY39mZj acBbAYYJSBtD6C08pVQFoLhkdPdnwIo+tbXXzpeuR5DfuKXy9YylPb7yh9PTU25vb7m+vma9WjFO M9M4V8FJZfxtvF4rKES1lro1tXY8lQ4QOJXF3S2y2D+9NcKnMXj2V/f9XqC74PE9IxTrAqs0CQ7O xa8HMQpFZiQUrm8u+fDjD/nOb33MB996n9vbK9abwZ5T13Ww4NYKanzNPAA9iyXlU9/WXEeLtk4d SERaTInyvySBPAzsgFfjyN08M2U6V6oBFZeXIrVJnr+y4/1mdtL1UArHvKuvTP22BIX6e/8I3Rx0 E9fWmSkIUQSZM4fPX7L/9DkyTl3RO4/uc1nHAlxUqnUEHR6r+NW05iV60ktnM01TtRxoD00IzCTc CJZzNg+WPqFrgEFEGylZHEYQ9f1rJLteuKTCnGZ2h5H9fsftO+/we9//Ib/7g9/lv/v7n/MnP/0p P/vZz/nedz/k5OyUcdprwE9QsKCtp7fc3DwBYJomcraAOY93M9tOCGpyHIbBP9CnLckWQDbmU6e6 Taj92bQ131QWDpkzZZqZc4E5kOfCsFmxXg2aP5ALEkWzKwbNU5Vx4jAWcuqcHyXYPOWKEKVbaAFF 28kmpxTrc5Jmcs7MJTGL+sw3QeMf8IwVCqv1mhgCU1CL0Yx+r1YELQIhauMou38WLyVumr5kyjxb z5WgDc3nA9NuhnJPSUKed4zTG6b9C3LaMnAgykwoWu9BmXWkoB1WvaB1kUKSGWTutEABBg2GZE31 skvi1zsKGrTp13/k85K7jw1E+gJADOQ4g8240QBjrsVihDTGoSBB29sPg4Lj8/NMzoGUTknpjGkS ttvE3d3M7j5zd5/YbWfu7md225nDOFLyRElRNdlVtrTToPFHcYWgvVt04FIBNVicSomUECCFJvwT SJYqKMOUyHPmbnrJ4X7H9s09N8+ecH51SYwDM81S4ize46sWjNFJqTjnAZkfxHw55Z1hfgVY7O+j 1QSVcxdFWGQJmrhbClIyCWGFsBZ7lWjuHiGg2VgKDlVK1Eh7A/N1lOJCTgGG03mezVmVc3eurxcT RGazroHvtq56YTGsBobVwMnJCbc3Nzx9+oxf/vIT/vL//Uv224PxBiwuSMcXYjAt2i2YzdraU7dZ uFgAtrc1jey/LTwG4EsFGm6hrgLTni2aRi945hyIRIrAyemKZ+885cNvfcj733ifi4tzQhTjD55K L93IAyLtGR5dFz6yo6GWxS8t5dqfV9sZeGi7Pm/GircFYcyZQ1ZrWMMIplQg1RLm94hxYLMWJGjJ t2nW9g6t2WUDclUmv9US9XBvHT/f4hn9d0cHZkHxmjTpMDHdb8nThOuIHqvS30+XcAO1haK9HWlr K/74+7/7ky/drGVx2Uc+7hmGUC223UP6t/KjV+qu6ejTp9DL65rWNO1Hpt3I+fqU9995lx9+/wd8 //vf43u//VusTlbqq7QmQCEG/uRPfsof/dE/4rPPP2NYrTi/OGe92RCCMM8z6pMt1sI3anW8qKZ+ R/1iqkAIofUW8YI1HA3d5itQqu/WpKIR0lr2ZkXcUTSDovkDrfrcoIGejgwxSwu4b9tNVcbsqk5E 0yBrNHwhkxet3EvRwdWGcQZEHDyp68YQOtRGWSKaJaD+yLCwSCmwUFATMgSJpsFZN9Jyx5xeMh4+ Zx6/IE0vkfSGgT0rZgYKsWTrjhjRKvaW+VADrLJpeIniNWcBKdFe6jJpWR1fLYgeP8qXvKiCoJos KsPt2LaPoRtL+77YmuhcMEVBic6pPn2MwnoVONkMnJ5GLi/W3NxuuL3dcHuz5vpqxdVl5OIycHJa WA0jEvaEkNB0Vi3ZHsKKEAZSypqpVLqxdONyV5PG6xj9Ss981HVAKmpR3O6YplkZYXRftGUBVRI1 Ju1KxjEXeQAu3vJB6T6Qow/7vzRugc7CKi1IuhRLc9Rd43FJCcVVuY7T4wpCBRC9hrmc64p66liq Vhf6jK026joFfYCV9NeQ7iI+eP0kDpGTk1MuLi84vzhniJGUZ8ZxJKVkgdqWFm+XadklDQz0log+ PdTH2ijt5xxlhPR/87AdwhAHzfLwc50u0lJnPQ5WAgxDZH2y5smzKz786AO+/Z2Pef+D9zi/OFUX yIOAmEaT5etL0EUls9P32Okr3ZzZGrCKwS2GRGN50hDZB+HNNPFqnNinTCrSrRepfDeXjNdc1NRr twyF6m5REWF70coVlCOw8bbjbZ/1VPFD9Q4fo/LcQQKSCuPrO7afPye/2ULSNNp66Q4ktQDoYyu+ fy7Ev/Pbv/OTt46Yxnba/w8fpHTnqd5Y9ZXuf0eU3cR7bEdZLhIx/78O3hal9TLJh4nD/Y55mtms 11zfXLHerCuwyFY+dxxH/vE//if80T/63/npn/6cn/3sz/nXf/EJh8OB09NTzs7PiUOs2lxKmfVm ZYJW752zlrh2c7K2Wu9M7ke0amu+PPaufr/zeSeLyWgBTrrwQowWi+F+fKN/KZrz7YBE3GDbJt1L guOMTCysT9o8zUULf2ldENs0XgFQNMswRCHEwZhdUDlYxMBVMCGv8ynO/IQaxKRLZKKwA+6gvKbk 1+T0iig7QlFQESUTTYOsKCZEimZV20btWXIzIXtGTQ3AxAqFSfr3AC782o8dR8yseMxL6IBFoRbd bzvAvq6AydhwAxp2vUAvjBWchJCJQ2ZYZTYnhbMz4fIycnO75umzE5483XB7O3B7u+H88ozT01M2 mw37/R7vRZlT0dowFki9oCkm0Ipa6zxKHqg+W395gSCKcBhH7u/vORwOBAmsVisr5+60L5WZuzw4 1qwe7CN5/IPjADLohaA9SSm0TqwmHIsDDF0zsUi9h2aRaLPAOWglEq/v4pknuoeoipa6CdtAKxBo o8A1YHcxhOi1Zixoc6G0Lddp6J7wAZ3EOrUWBYoXFxfc3t5wcXlJiIF5npmTAsvBTPnH99BHam6P lrnRu0EaoOhBiAKK3mXi7vtlBogGeTtYVh5VXR8WqCmG+ERgc7rh6bMnfPDNb/Dt737Etz76gCe3 16zXg7mHc/vOo5R5TMQ9fjRa23MeASasSFqd11r91BSuAkggx8i2FF5MI6+nibEoj9S51TTonLWS csrF6q8slTV1lXisTrPGZK/My0Ng8TZr0ldRBnQPuTtQRCBpcaw1gXKYuPviBdvPX8Bh1AzAhfq4 vGbgIeU7iEb8w+/97k++bDr6iajbuNvkj5lrCh6t3g3Kg6QK1afvfy++J6p1JzfHmxYXCkhWYkx7 bVe8u7+HkjV1a7NmmubqEsk5s9+PzHPiL//qL/nZz37JL37xS/7sZ5/w8uUX/K2/9bfBrBb13lYn o/drDatBN7PoQokxtmd7jKqAm82bcVhp5rnhMUTtiOgtzcUjpw0sWOEtL387mOCX4gBlSffWiIxu bjpEaf+5rzeLmshna6VerLIplqbaGICWgQ4h1oJlOTuStShsyRb7obEogcQQgDKRyz2JN8AbRLZI 2BJkIpAJnm6ZpbrWggChAxR1qXULTlkedC+pJxYV7KFtil8PXNjNypdvH38V53gLzid1vP1bbVMY SJKCgqEE5vLJWYtuIeYmKqVqEKpEqSslDrBaFU5O4Owicn0VeXJ7wZPbJ9w8ecZmveHlq9eMU9Zi PwkaS/BA3Qak6nCLZ6UshgtFOqteC1Acx4nddst40D4l69WKYbDAZdQd4ObefiofwxDNJP1QU1tk SB+Biu4DpS0tPqYGtTnQ6O5YEAv4tFf3fhKNX+oDVJv1QdT1JN0D9bTC3XsujM3XHnrDcT92BySw 1AW70dqzeT2fYCmOm9MTbm5vuL665ORkTUoz03hgmmblO0Hq9T3wuM5BORLHFidULS2PvegrXnT1 KqSBC89Kc/4eg6dvNgFaJBOHwMXlGe+9/x4ff+djPvr2Rzx5esPmdMOwHoxnmUQpVmOBRvaHYMKu /eC9YyZ9vD/b87BYd3XTNV6km4A5BF7PMy/GA3c5MRdtfpbRsgCpFOZcmEshUWrNFe8pVexB6j+T DdqX5C3Wl7oU5NHfq3XhaJ8JDVhUnUwXG0OGVRbmO61tcXj9hpDmZnnv6C2FRYfbur8e0A3iH37v d74UXCwfKHTz06D3McCwkMqm+fjLKPpIimyH98GboVGo5jSP5vaTBDiMB7bbe4YQOD05AUPziObk P3v6jN/5nR/y27/9XZ4+fUpKiX/9F3/BRx99i3/wD/4B0zQSY1QLRYyLvG2P2QAsdbSBkPpQjxKJ qnl6yef+VE13kwa2wDIZrR+DC3kxn6S0VE+3YuRO68xOjNAYsoKxRtF6tiHw3m2V0cU/l0wWc2cI CiZ8/NH7hZgZs6hBOUgiiAGMYIBBJkrZU8qWzB2Fe2CLyIEgs56LpruWLJTs0fUGfix4ttgz6Eaw RePMtnRLuzL7Xkj7ub8muPgVQYW/ln53n3Gpfy+YWh2Og4vefaLjF2v01vpF2GT5fstNEKnml5GQ iFGZ9Xp9xsnmnPv7HX/115+x380UaxTna7JupAqE67DwQkPNONUltxaQoJ2QU1GgIUCeEvNhYtzv meeRQmY9rBhWXnbeLFuhE2wsd8diruThe2+PueiBiGXOmJWi3kcAWtO4YvOmeyJQQqhp26V46qoC cQ9CrwH23TTWtVfv08bRnlDqc3rn2V6JkW4+VHha9kl3pwUPcStjaXU0YgxsNlob4+zslEBgnkfm eTRLU6nFw3w0y/F2dLQ1d+wC6a0V/v3jc/x56pOLWhta7IgGBkqE09MTnj17wocff8i3v/ttvvGN 9zm/OKtFEXXJ1w2gc9Br8dLG3R8PxfJj+7Z/9s5a48Cnl00mlJGaJwYSOIjwch55MY3sSlGXSFGL xVw0kH4uarnw5C5EHoyvBeirQhliH9tHdY/0LsbF0x2DcHtkBwJOAQfJLpoFlatDEcKY2L18zd0X L0jbnVqTXcm2/50naCZZDzCaVaufCgMXS/zcT8njgSQVVSyIs/iinSZu/yttYL32IJ1rRDe2NQSr jOGhcBAJjPOkQaDTxP3dHbvtPYOI5RMLw3rFEFfc3Nzw8cff5oc//AE/+tEP+d73vsuPf/xjvvnN b2r8Qw0yEt68ec0vfvELLi4uWK1WrNfrOrFe6z57xL0/57EcAUOey5H3LKdY749oTCLnoi4SjWiq E9kKzHRlvGOXT29mttL5IqWgi8Xo7OgYkQomsnHIIppVMqfElFJtqhYdGNlGEBHLKPFYjEyUAmQC icCMMEOZoWxJ5SVZXiGyAxkRZmX2WZdjFC1+pYIZtVYEy3IpBr4QdZWIpT95qXCVULTy5A54MwR3 hzzGQL7G0XPPR15Fjhezv1rTsIUEapLSJqkHL30cd71BtSvU98XDqkpzYbn1oAjq+hgQWUFZIQT+ 8i8/5a//+jmHEYp9poW2dO5cE2y5YFItFr5+6po1IZuBlAuEWK2EpKKdYYE0jry5e8Nh3FdeEoI3 oNI1e7ylv8qf7MdjgWpLUgtKOcvL99GL1LVeC2zZq85JaSIxB5glkMSyaOraczftEXDpxakLwsUz 1dA+oLkVPNjR01ddHajxHt3zLejgY6pxU0bnGNicrLm+vOT25pbrqys2m1WnKLU6Kb2bw+eg9gKR NifH8RXu/uj/bpkxHtvRuVz82czEvhoim82G2ye3fPztj/jOb32HD775Da5vrs0tTVXycioLPtnL ox7PLzFD8emsfO94Py/XWweEOmI3JVKaULZ9GGwPb0vm+Xjg9TyzB+YslAQpO7iwztfFgUUb88Ly 3AEAp2NzVX/1vnh4jq2/fu30wKI+T2EgMGRI2wN3n79g9/IVMnnNEB9bB55tfy2Be5OHLScK4h/+ 1o9+olpQN0BZ/Hhk4PV+jwKLHjj4d5xwfonuavqzo4K35XazrSP0bA9bUIaVksZXpHlm3O6Y93um eWZ9ctJ8nGgvkqura957/z2++93v8O677zFNI8NgdTFs8f/Tf/pP+V/+5/+NX/7yz4kx8N5777Fa tc2pmSRfctgarpaFspz49ohhsUmUV2TNDMlaTjuIVj1ULcf8wEGfe7Vaob5bbVrWQlak+ZRpG6tW DzSGqum4xueDxVOINWHLM15xUGJUJuzgSkCksBqEKAnKRCkHStlbPYY9hTsyL0G2IDNCNkGlaaJC 1OXppkGB2rfCgp8CUddO16pdc9/1/sUyXqT2cfBa99YRrjSB/eu7RWwCH3mVHkjUYM3cnfPYPXtm ZhlSWNAYgKfSiloYaqhU1ah8QYGXJi4WqyGyIrCuwIISmefCLz/5d3zxxR0pD8AKGGzeC9AsFsVU naZrN5YLzoxkQYVKHwcjKCApRdfkYRrZ3m/Z7/cIgc3mhGG1MquLr89G5WMh0VPweA/VkR2DFKfu wqXThIs/x4LrKiErcEAgh0Ay13pNFaTP669SajlOmycHa67lF+W6TTB5PQ0XvsE6+fq5TdQ9eDql R1sL/cdOp2EYOD094fr6iidPnnB5ecF6taKUoq5Yq7vTA4k+MLOBj3ZX1VStcJrzFXu1woX6rwEN FjV4VquBs8tzvvXRt/jO977Dtz76Fs/efcrm9KRa6dz94bwzSL8S2y9y/EZHCKP+gmb1TLtue9Im r4BmIOy0cAehgvKcQCRL4D4lrcqZMwcJTKmQkipwqQZyNnDuu87dIj1CKrYONazOa54Es6h7lmBZ yNu3WTKcQP0e8d+9CCMoy1xJIOTCdLfjzWdfcHijtS0qr6uA1Cwr9j0xSh/fuXTAZvBQLnoi59KI /MhRsmkItrgW+KIW1LGcchy1+ZQXRdzZCteEoF3rCEjJamYPGnGexdiW0AXKqGVjZfX406w53BS4 f3nP7jAyp6IpcpcXyDBo+9g0k0pmWLWAnGL1EoY4cHd/z7/6+Sf88//z/+ZnP/tzzs7O+a/+8O9Y hL3WxkhpappJm8UjEpfKSCrTfEhB3QKlQLZATlHGzJTJs6L2vM6E9ZowqJCPZHJQpnUaV8QVxEPQ 2JJp1oVT1JxbaqqbClv1ZujYo7jgsqwAE5KpFOaic3OYM5uSWIeB1UrnirkAifVKNK11PgB3ZHYU GdHS1COg9RUqAHXtEAMHVklSebngbcKlAlcX4IIH/QpuXxM87E6Zdj8Xi1DyX/t4oB0dHa08+vHP 7vdFgMByJznobm6HlhlUn1WKzpcHHxYoPSDwbFnRkt0OWnIWiJH73cirVzumSSApGAuxMJdJb5QB UdeXiEUIFM1kKiGSzXTh86NgT4WgukUSJWtcRVZ0TsmBEoEQyGNhPx2YD4V0KIy7mdunt5yenzOs oga5ldTiWS0/RUyFDyIGVnQ/eVp3KW45VGZbu5hStx1a86QBPbEPVLjYuup2bU1hrN/wuVC3yIRe M5XCCJayqnBtRSEYfWqTLPH0V3w2a1DpUtSJ0j9ADAlChJjI8wwpU9NHcYGoEx8cLB0F5drDMCdV mNanK243N5xenvLs3ad8/tkXfPbZ53z66Wfcv7lnHmemedY5d5DgMUzGntyyW3uo+B19DYNmOQXb y+JzoOPKor1oTk9OuX16y7vvv8sH3/yA65srjamIoY3d3HEe8FpdPz6XArjKWZYWyuWxBBb1r55c mJXA1051MXn8Ta7AJgAkk12i63suME6ZaSzamTrATCRH0d5a/kjGywoWOOylBRaml9INq3E5p2eI ZqUlkHKqze5ammp7cg3Q9wd2y2aprZEGVzBNRhQ0jm67veew3zEUIHktp055KB3f6jhVabei4EUD 9Lnj3/6tH/2kjeYhQ33Mv+lSszcFLs8r3XdttVVEacU4uvk/xj/SUcv7OlR050OoPbx9EwBFSKlo yuo4kqdMkMBmswYDJQ58hlVkto01WEzDZrXh/fff4/bJU/7gD/4GP/rR7zAeRvMZ9pv5mCJtES+0 mS+RcSKN1k6K6sOyidSyt5nar1wVW8AzOrS6Zwxa+VLMN55dbXALRA2/Um05WHyHcSsL8im14JAG ImnuNVIYRHuAUCaEiTTfk+Y3TNMbUr4nyw5kD2GEMOGVER9s8j6FV/pPmp4RbEw1bkJM0z+6li0U ltkYD8HFr2O5aOv58Vev9zz8KbVGgmuyvvpbkO0y2LkKHROilNIJRKFOjGcO2SKo7obs9xqAQGbF p5+/4V998in7XaDkNXhRKa08glY0dNeL1a3xvYkDuaOgbH+vbjgWTC5jxfMM2EoJkIV5nDnsRsZx Ik1J3XwxWp8SA022+L2CrwgLRlXnpfvZdmNgmVWkvxbp58z2nHGhulIemeLW08WUJ+NR7gRMKOio rhXaHAeXYt3epr7V5tk/Va1Y46wUuD0UmFW2Ot/thKJToousIkghRnVXFCnEVeTi/Izr6yuub695 8uSWJ09uOD0/Iw7RNx1U2mj6qM6DdPFe1HmvLh3xIHALQqyKgcZVnJxuuH16ywcffsBH3/mYDz76 gMubS8K6vy/2DLkpGlWJe5uK+5BOgFl0dU04ffrg0/5vfP5p4AJRt1g1ihYhZG3aFYvy0sOUePnm ns9e3fFqd+BQtKDWhDCZq9HBRbbYi9wP2WlrgNNFnIkwqlVoMU6n9THIW1y48dcKxmz9Fw/EDHiq tcODfBh59elnHF6+ZpgzklK1jnnVTi+mFeu6qwRfWrzw73VFtKhEXs65I9f+Zw0u6Uxj/lLE+TD4 pJiE1u95Pom1/JZSN1YpuplDKTWopL9n8YqARd0FFVlbBLsUYdzumcYD2zf3XG/vuXnnGScXJ8TV ijgMIIU0z7qBQtD0yhj5vb/5N/jeD77Pp599zrN3npKTZkLklMw9oVkefTpqJZ745m+o9MsKnzx+ uEarK63kxHTISJoJw8BwsmZYr4gE5pwoRVgPkVVcMQ0rDoeR/TgzzrNZJjBBkCooEdFMnDbptqo9 9dPTbnMm5Ykxj6wGQUJiyDOSDoy7l5AO5HSg5BENQlTcWte0M+ZOMCzJJQv6LNiEbYoGLNvYG12P afv1QcSvejwImDp2oB7ffyFBHv58uCocSNnnbefqp0fgvxjTV3NptEwGBSClBOYML1/esduNCixs XlMphKHT0kuqIFfdkCoUMtadNbQ97IxPNbsWp7RQLJyZzYBlB0gppDyzm7dM48Tdq9fc3j/h8vaK i+srVmdrBCwtunTm+RYJ5s/sWtRibnqKdjKy9H/4uaX7xmPrsl6zLACAggBqkSStG2PxS2gmwEq0 ++9KzUB4fRB3ieiW9nv6GnI+oveLWKyNRMoApQRySuRZLUq5frd76lKvinPVIO708row6uI8GSKr kzXXNzfM08x2u+PuzR13b+558+o1b968YbvdMe0msDxSKgAAIABJREFU5nFmniZzoxQKqS7TYub7 fv4lRFbDQAhRg4pPVpom++SWp+8+4+bpE84uzoirwTTmYqynNO3w67LLfs66eWxCTqkii7nEUWbb x0LNBinGm4KtN3KpbRbnKXO/2/Hi7p6/fv2Gz7c77kOgXF4Qzs9YDYMV18pMqNWvmEz0gH3nezWO 2tdJXSNUHnrsAolR6+F4/F9KqcYNVpC/IKPvU9+aLYZOl4fy0XF/4LDdkaaZnLRZ3GO8uYKWxUS1 dagntc8G+ofomLcc36D7u1Xy7G5Rmo/RDw9+FJFFJ0Ynqm/yYHUTwkIDyeZCbxf0gEr1/+oCX0Rd i+AFcwrC7s2WaRzZ7fbcPnvC9dNbTldnGpCWNZ0rAxJEe50MkdPzc75zcUGIwmG/JQ7abdI10L5W RSN5i6RdfPJrgYxmagKBXJjHSXOOBWIpDMOmZpJkCQq0VrpwYxwI+5EpJVJSy4wHSJWSNXJcbXs2 xkits1ACgcTaM0DKjKSROY9Q9oxph8wHStorYCmzCaYIZUBDfTKRVBfZbxLz8FZw8h/p+PXvWX6F 3+2dqrYs96D/WlhaK/w6pc6Z4AGgQqDkwDRlXr+8Z5rSQkhm627qc1PICsjFb6vqlJCoAaO2PwET VI25kbOa0PC9XnQMBaRokyaCz2MiHQr7eeaL/Dn323tudnuePHvC5vzEbAnmgrR0S9eckZbO6hpS rUsHNuZHyfuQj9lpXzq3pd+FZcFOW2SNaqiZos3QDFwkEdZFiN7kqV6jdJH6NnBLs69FvkwYSQjq 7imBZEArp1R5pq6bBugEb7eg91M6GT8OahLKLsAFVusVq/Wak9NTrm9vSFPicDiw2+057A9s7+7Z 323Zbrfc39+z2+217XtKNfOkp28IGqR5dnbKyckJ5xfaMv7q8oqzyzPWJxviyjpAC3Wk+jxd8H4H or8WxzyaS+fFD95EOXXbEtJevcJQLL7E9PSQC9M48eb1PZ9//oIvXr/h9eHAbprYSyDvDoSriXh+ zul6wzgUJikWt6P8VudZK5GSIRR38zk9m0ytRpujo4FTsQDppsDp3CzLVKrCIObS7KFBzf1C5sTh zT3Tdq+RqEUBviugD3fJ8cDKwz/tS8NxvXIRMf9rx+cee9DiBtSGv9vRvthAh57pbDDnZv7qGVgQ UZ+vEwiq75FMrZwJQbMMcjELRjaTD2jbaTVt5nHm7vkrxu2Ocb/n6XvPODk7Y7UakBjZT4fqLimi AU9D0JrvWHyAm+h8jpYL14VD99RHM/KrgIyFqcsu5lstFGDOJCbSnGENDIEYV8qSjWmsVwOrQQOA tEVzYUoFz6Ao4m6h6HIEDbhU83RA019XeSTmEcpEKHsoW1Lew3yPpD1D1O9VAFQdOl7yrKB63pcf x4D1PwWA+A9zPAYoHpt7qZrocv5pANAyS/oQRbf4ufB3S4bSU9jeH7h/s6MkBYzKN1V452QuCDQ2 QOfJ1kf2RZwoFhwb0IJ2fQE5DVbLhFDUJdOZayVndXe4W6/mrWmmSCmZw92O8TAy7SfG/YHbp084 vzpnPQxmPlZBmLzuTNC9XpXcRopfSQg9BBjSvn+8J4v7mhVMlQVTFjwORfmFCvKZwgQMotFGSGDt fMJSLzNGB+OtxQFRm3Bciw1W0bOUbEpEIMewaITYmJCtAVBggtSeSA2vulWDqui5FAsxEIfI6nTF 2eW5Bq9PM2lSy8VhPDCNE9M8VQGWTfAEszYH0c6fm82G9Vq7VG82G1arNQRtoFgsu6wXfi4f2px0 7q5fUSF7ACwUkVenSOnWieJwp4tb0akuEbF95O6DQSLzNKl159Udr16+5tWr1+wOE6WIVhVOE+N+ hv3M6mIinJ0RzzeUUy2H3/qMtOjGJZRo5DjOEVMydLEVdhXf665YuxKfUoKssTpYXSS1oDXLYx1J 0eJZ+TCxf31HHic0h0/XXT/KxZR9DRb9IP1Bmbwj84fXesxEvBzC2xGJ9otrfpmcG1Os1/PFYRkE ujHcL9W5ZACNlNf3JZt0LOpfIpXWgXQu7F7fM88zu+2Oy+srrp5cc3F7wzquyWSSlUuOsU3YarVi msYKfPBg09Q/W2/A1ceXIg8ARk+7r6qwJsb83RRcjaezIt8pH5AhwgrCatDCK6JlqUrJnJ4MbFaB 1RA4jAf240TKScdqwEKpV4z3J6IUIpmhJG0glveUfIB8gLJD5IBwIAStbVED/AoUEkUeX3W/ieVh ma71Hx98/Ppj/wp0r1e1n2Z5K82/3z430OZuAmNSWqui1MtqBpCYTC68eP6au7sDZDfBZiSoRjIn zP3hTATwdF7vwaEb0wSRMd1gjMqBtoF8ZXC6Hqo/uHhZZ5OwOSDRhpyBGGBM7NOWeZo5bA9c315x fXPNyfkpYR0glGoJLEUFcy8ke8q6nO2tF28D+D35jy20pZSH15IlPCxunS1qpctFAz1nMhEhFbPe iEXWd/m8Yrw1VBNnqSC/Xt+EfhW2VsAp5EAJiTQnq5lggLC0gEFciNjaatafXC1Abhov5Pp8Yg8s g2rrwyogrG1Qxcz6uVYGdWtbsNLedppd3hUtMWAFWPxHFeQd3UUcCNmoqxXhq4/HgIXiTqdft/PE QanUFVRkOd8hWMp/Fspc2G7vef3iFa9evGR7d8+4H7VRXC3Am1kVzQgZ36g7qZzcw/UFXJ8TT9YM Q2AG5tJiOrwkgAOKOsQuRnUhYTotvzl82vtuxfA1lLOpH737pd4XlZdFA5DH7Y7xbovMSa0rYqUg Fuu2w7K4Uvolh83f0i1Cm9t+sL6BG6rqLy2m97R4ixaSIEfn6n2s6DQhi55v1hLQAMzkrbNrCo5F tgY18YQgNSYjBM0aUKXAtlgo5pMq5DwzDFrlct4eeDl+wbjdM08TIJxdXjAMAWTQiqD9REr3GHX0 HSFqIF434U5EeUQr+rqHt+WlIV7JUPJs9QXMTbISA0XF/OKFYjUxVAsaOIwjKas53Vt0RwpRNEQt 2HtDHhnyGyTtKXmGPCJlQsLMUKyTrcdsOBS2dufgwsfsTl8THDzmjjv+/D/F8e/vvran6hJz60T3 0+SxrsNo7gn/jlnSbP25LqRtENR0PqfE889fsrsfyUlspwkpzc294VLMU3h9g/tm9/gl15DALHg6 Rs3wsvOyNmYLoagP10AuhUXhOQ1QVmtndCsEhXSYeTW+5O71G3Z3O9597x3WlyfEjXYIzmbBMGUM 3wnLGWluyUrnwqMAv86CCfFH12XVpl3Yd9Nl4iALaqUTbTInCDNuryukEhhEo/O9OL1aCcuyBk2v lZrgqzzFBbUIHiEYFZ1YLSC1DnkmnYn02oeotgfoHk+7QFNBBg7XavaFPmGNH+uGou7VuKSkMaaF kDRJ7q6aGmPg4LPj1X6+SKPHw8iax46lDGrP6APq1kgFVk5ne9/WexRNsaVAmRPjIbF9veXFFy94 +fwF+91OLX5F6Vcy5DmRUyJKZBMizJnDeCAfJuZxouz3rK8uiOcnDKtIFpiDVeqkjcfjc3zkYtvo oV+nPedxSqo+oraNiCgvIFkxvoUS0gBWECGPI4fXd6T9npgLwcCjuGGhdGvxwfHY+Ar97MU/+M4P ftIP8OHPBhCk//3o3OKTWH/0wGI5OF+30n0m7evLhdcJ+1L/17/7oMAlrQ2hFx+FVGQcgGkcmSZt 9CNADJH1eqXuPwNIGIJTbfLIYiLdM9rA6yNDjYj9VTTex86ppi+cuUtdePrTman5orP61oOIdg4U n2SNwRjihhgH1WSzWkJWUlhJZiUTQ9kTyw6Z7wnpDWF+Rcj3hDISmAiiJrSIQNF6DKaXVU6tJuQM pMf3xVc889uO3zTm4jexmPz7iblYjMY+9b3hriQrMCVeFXVQ+soARBMyoQH9TpI6YxYRUp7Z7zO/ /MWnPH++p+RIkAEJkWmaKrjo1YZSN2I35q/CUraBa+AxjzG7poosMr6yaV+l8YGcC2lOzNPMfrdn TjMELYiH73G3Yj4yJcdFfZwuXymjjG6LmC2kpRGigtzpjvR3oAqtwvJnKuZs8vFKN9tFffju6PJU Y481a/fq+W+Lp/B4F3dR9cWdQF0VyvekzkHx+TKrU3tefzWeCh5T5kH73WP7mKTjxqUVjmvnO/81 JcRpWFwjLkfgwtlob8WWL331sunh78UyVvTth1k9UpXlKMKAKlzzflJQ8ekXfP5Xn/Pq+Uv22z05 aSuKlLIprAoA9FWqxUvMNJDniTxOlHGmzLN+HiPE0FV+FUrPKOs27GNCFquVQuuaWt8r5eEa9gqw lgVWrRaCWmsEYobpzZb7z74gvb5nmFTBpKiyX0oD7JVm1ETgCiabtaitCf87/sF3f/iTX4WFLkDE 0cP1n/uiKt257frS1lTdp6XbTfoQ3pGxCi4ftm96tcU39tj/bjfzOARlZMWYmprxciqkNHPYHzgc DuQ5sQpDLbNdQYz0/k8j6mKn+a9159Gb3b6MlsfuoAVNWU6aT6pXqfOJ1n1kbeFTtvGhC9kKZwWi AQx9tkFgJYlYJoZ8YGBPTFtkuiOke2LZEsqWWA5m0bAOnUXQzqMmDIvVmQjuRtNERAd9X83Zf7Xj y+j0H+r4ze95xACcix+BigomxECGRDSddEArakY0hXSo27mI+6WLtdZ2n2om55kXL7Z88ovP2G29 gqdZPo41dBFqIGTHEKAJI3/jeB/3lyn43loGVbpWnEvpgi9bcGTvh1YLoDZW2+/3bPc7xvFQe/kM q5Xu22w2UvMn+40q2KYJNjfP+7hdBvdHPyO+q6pmjUX6y/E32jelfpN6hWLfSyJa4bOjrxeiWlzB qk/Ws0R4uPS6O9Tql4ABjWBdnf2ZS14KBp0zrwqqsS8tiqdUkKHppaHdB+dTrUjW8tn7WLp6NQqW UWNrqC19l/ZfNg+LGXt0L371366lC1jBOfcOeGFFMWUpFqHMicObLS8/f87nf/05Lz57wf3rLfM4 a90mA8BzLgocC9UCkVBZU6MUzDIVkqZ4TvsD4zRZID3a7bp3EeHxK012KCD1eWogUbN2TKbZXLcg A/9PGkjGC5xRaVCw8U2J/fOXbD99DrsDMSekqzPis+A1VXxbtBTubl5Kv7NdbBetmdGYyuNCsZqq pXKJxaT2AOOxNMHulnZ+l6lhA1bGESBogpcFj6spDWpcmAeoZ/cZmu9TclFGii6sWg3U0lyjBTnl WTdayMCYLdhzz7zdc/vOM85uLhhO19ZrQCtzZrR2v1aQ1OwLL1jV5rTzh32V5vcVx3Fwn/YjURqK PV8DW+o7y2lmKomcV0gR4mrFytIUSynEUFhtIgwD427HtN+T0x0h75Gyo8w7JCeGMJnFouDtwDVo ry1krStg8+UbpJcs9Kvqvxwe2d8YcQuAbTsA+6nWCvHzggPbrECjzGiupwr1uubNrP3i+Qt22xHK SY3BKJLUfZgzhdAlnvhe9f3tvTcylpNHFQYi1vxV3ZQULOhTIWwhWEOtBi+LddCVbBoTuom9Yqs+ Y3acYwGlhfl+Yk4T4+HAfrfn9ulTTi/OrGmgnVeM34DRs3PFdfvneBW+bWv6vnr4raWG3V/elXAx Ovk11OzdAahie7ao9jjE0Er02y281on4rLwl3aEZgez8qBkNqlkLuSRtfGhaaDFN1B9lqQyWSrNe UHjcTP/4vWioWvIDWulfNXNFXPnze9Z8+C91NVYw28/jW8D+45Zfm4fSxuj8Sy0WVik1Qxon7l+/ 4eUXL3j5/CW7uy3zpAXhKA4cFCQrkLA6QJTqBvP3M4limSBWrJw0z8zTSDociOM5w9U58eyUuFox i1byLKWQQ4Rgq9iZe2WnjYahWqOOeGz926WtAlfvTlwkU4palWPR7uLj3Y60PxBzrq4Z318PyVq6 +V6uncZHGv0Fj7k4nrBHp5FqKvH/e5DRFtDD7xxnlfiAPDbDYzAUIartYjEe8WAusWhWzwrJy89t EspgY8yGXgOWQeJageG/CKTMftryxTgxHSYudzdcvXPL+mzDsBp0Yi3AJVip7Aqq3DRdSfEbogp/ 3kcu8yBAzU8UNQsXMiFnSkrk/QRJkEGL6YRQKGWmlAMp7wjpNTK/hukOykhgRvJIIKuloscIfi/3 0Re1LOkc5m4ph7cw5/8cD1nwhibNLV20K/HdXnqepj0PFCISovWQEYJYK/Q8KeMjkfPEMGgWgaaA Fl6+eM14SOTZBlCzG+woULKrISoGxMbmgZyGY7XipEDNNvI4C+vkK6YZQiKj8Th9AluN16h1MYK6 8siIREIJVgAsKsiw/cWUmRl5Myf2uwOH3cjt01vOry5Zn6wJ0Wxk1q9C63Fohkr2pmuPLcFyDCKO Zq1TFhZ+edqKbvyDZhnofrZbqQLigXwZmAUOpbCWwEqElTF6Ldff+qG4YHxsnP0e65UZEes8Olgw bNZ4DG/dra/cAQkDlX5dn3S/cnfz0tGmo9bjRKzEeETolyWRKu0WisuSjg/u+hUWxSaXxfaOCstQ pFZ9lQx5ntndbXnz8g2vn7/g7tUbDruDAtds1vfitUysyykKMFKBuf6uZenUkmE6fwbJwhADq6zu lnnyWIyRcnFgOD9jtdHGfrPAlJP2fIqie6JLGuhjLLz2zFfFgfU0DcPAUAokoZREnDK7/ch0v4PZ 4u183/duLnyFlAaEZTmuY2Dhc/AouFhgdr9ph2yrEC3NfHcciNe/312MXEGAE6xDqVALOEWPWLVr eGBntXYYE3QEvPg8ZySpTwvRqHZSsXK83gpY+42EQs0OSYfEq89fcH9/z/12x+WTK26e3HJytmEI hTlPagGwsuNNq1sitmMa/qZHnXQ6EFPMp1g3JpYSZtpRypQykuZCGAQZMuSRcf+Kw/4VedpRDltk HomSGYIKL1VEBfJgkdTNV6maZ6sxos9b1ajmJvkvVguqlmm/t0ZUfbquN/KyvhJFhWyx2AsPAxQC EgYDkYkSVszzASkzmWBxNFqAarfdc/f6QJoxYN35Ruldi5a6kXurEzq+0vmCu6wRRHBMAq4NRUQU YAbQ4lqd9crLN2MAX2z/FQoSigIoaTUe3I6jzcoLkgqHaebFlNne3XN1c8OTp7dc3FwRo1bMVKDb UfuoTPXDWfHR/YozKf0ed1r6/8urLO5qZo2MAoJkmm5AGNHS4ZsgbIoW3oq5KF8zi0M1Evcy9y3g vb1tHTU1Alc7DuekrqmcKSVYBklZ0EynSI6uKIu/Fk/6JcRzt13PtxafL2RC5R5HZx0931cAiuOv KiYWU1hzTccMBcqcOWz33L16xfPPn/PmxSvG3ajaegEp1uMoZbzirIMIL5hWa9y6VYO+Z0i7jlsA BqDMibTda4rv3ZZ4dsr66pzV5RmrkzXEyFQBSnNtLGhHk4tfSgKPaxILPLYCYaEEYhYkH5jvd0zb LZJqNS+7g2U05Qakl8uurZnHgEV/LOpc+OnZGIJANastM0qOUsLeimp9IK650Mwvpe3Z3uqgkdF2 76xaiYMOPxcxf2jwnhla46IWxrLmW1mCyTplaBQFGR5dXwrUukJByHPhcL8n5S/Yb7ekw8yzd56y uTgDBqsiSM259wIw/yGOxZxUEzU1crt+lrWSonnlkDwxSFH0nCZySoy7PWnacji8YZ7uCWUi5hmR rKW9zWYrZr8tKIhzgaE/NY1Ym8f1Dx3wqDVx5/pXRXT+//xQ0Whp0rVykmeDKLCgaAM3pZ/53WUw kBwpRYu3leJgQ7VzIRNDIKWJUiJzKto1smRevnzJfg8UDYTU9Zlsp4bqnsGzC4KyxWqPdIYR7Lvm 9y0GMiVgrgsPDDXBLsHchUE1n2R7PphbBFGjF0V742Tdr7l4thcGtIActE5DbkOYy8h4ODDuDoz7 He/MM2eX52xOtRnanBNz1t46mqFSfsN9uRSnHuVCx6/sFyVnKUtrh9HS/f7Zar8khIC24Z6KP6Iq QqsAEKxPSQPwfp0vOwquaDRxpOENljFmFp2cM5Kt1knXDfcBuY4sDE6Spjw9FhdiIKXSD95yYV1T i/O6T5ei49H4ii+tF1QsrqWg5Qiy1qwIBNJ+4v71HS+/eM7rFy/Z3m2Z9qMueeeg1s00oeteXSL6 vu+WGl22ABduHWrxHTklsmia+EoEmTNlOmiDzd2eabtltb1gc3vF6vKcsAqMs9fGkIV8dBCeejlZ 6b4EvzWtHbHyDyr7oghrEaZxZrrbkg9jV6bcgnMp3fy+hc7d2/LwrXo8korq5pGy2DzHDyPileBY LIR2qbegGrGmKaVV8HwwdttNaqVQLb12x/Pza7S0+bdMM/IULTGbZXaFsTivLJQSGGTQ9ZeTmnBi JMaocRb7mV2644spM+0OXD+95fzqnGGz0oWUk1XQaxv/8cn+9Y5jmjTrASA9E9EBBKQCwFImoozk PJHnkZQP5LQlz9q5dCgT2iAuV02xZNVsXLPMvtGsmFGuWtjRInYB5sKmVjf6zxhcFAUQ4t3oxAOy PDbi2CUS66tmjGAIsmjCsMa+qKDPWavJ7g8T86R0/v+oe7M1R24kXfA3wJ1kRCpTUlX1xczFqb7q MzfTPY9YLzvTKimX2Ej6AsDmwhYATjIiUlL3+Y7nxwzSFzhWsx+23h/2QAEevs6YztCySEcsKfMT KQijMi5wXaO242IjzGIEoVdVbePeU9D15070Whwr3tY1yOoxQKT3i6RE6ERAoALwAMt0ywZMizBF KzNo3Iw0LXj8/A1pSfjhp4/48a8/4cOnjxh2A+IwInNGRnHZ0O8+qO6mPf6C7gSvrm1qe4I32FrW BFsmWwViFmeicEEGsAchB2BEwGBuge+oqhm3G+E12m2MmhTkEgcEyighIAZVazaqkq5d5dL7Z9ve q1fJ+ktuEMkBb2/BNVlFW67R+Vbd/T1H0J2/hFMvyEvCdJrx/PCEx8/f8PL4hHWewVlCewMaG0JD 5BdSEKEMXsK9c2O4Sb4JZzaHA7WdgG6cyczbAS7kS0oCVQFpXrEsK9K8IM0LducZww93GA87lDg0 xsAt/70O6vzomL5uwhUIgQsiAihnrM8nrMcTQrYYSh6WT+ZvJ63T8ogu5vZNaq/PDtcGjq6AilcH +OYl3RE1i5LkBZvy+gJaKYa8v3jQHlJw0tfVOLwGSybpLAoMoiiLJej+jTT4TVphVtcxiKFYygVI WRlDwJxP+DJNOL684Od/+Qt+/OtP2H+4gyU2koZBgEwDu/8oa3XpTgtYLiQXpJGcCVEBRykZSDMY J6TliLTMYE4IyIicIQY9Tbplrla+FKoOnTXCaV3q8j6uTbbulmfdu+fPaf//3oeCC5iUolp7M1dG I0R/ENUBJOGYSYFsV1S3BZKQL6WMlBJOxyOm84R1WTAMEetOiODzU8L5XFDKqO9j6B4MVXohxpda MOpocaUnW2bTjKjXiyFxZ4hgMRIkhoK2O5j60+7JYsRZSEWFERyEYEt+FJZsyyRhw0ceVarBShjN CyHLrvN0xMvxBT/+9Wd8+uuP2N/fA5GqFOG1IbIm3CDU1d5gW4p5RtWxaXpPV0v1wqhKInHf9vJF BISMjAUmbi/YURCgoTtM4h5guE3IhQrDLkAZihq8N3dIvQPMUMTXrgVaa3TUTKaeqW2z8rktFMZH ms1p+82ARncovHA60qvcSYGdeapsj9u79rafRMWbc8Z8mnB8fMLT1wc8f3vC9HLSDKcEzgau4NIJ Aw0m72MDFazeGoblFLjwJuS2wnRJy0CqOtHU60QEClEiY7IkhEzTinl9wHQ8Yf/xA/Y/fgJ9/ADs dx6UzrOgUuWNrx3WO4UZRZPXkY75ep7x/PCI9TiJ4JLNJogd1Lkb6qtvuX7U0BB8K0IntSecaVwt TCtx2WByJlhDaMMHzIruJmajHmnrYlHDLFUsKdAwz41iC0QDasmbsi8qM3ASdQqDaQDnhKjZGQsz 1iwRykIIrpvjwig54zk9YllmTNOEH//6E376y0+ABvUSjYXpqowYvncorh9Ul109xz0xJJUqEFvi moy8zEjrM6g8oeQJZAPMQqAFCAUfr8IaojtIn4por1wBf1X8046Z7zAMljsN+j3T8n/Tg6tUycbI d2dkf6kCC2U0HCIiDUBQyYVdd4JMKpYVScU8rTifZpzPM5Z5RkoJecnY7yNOL2dlMXeIwz3mKfuO tDRgkG0e1D1RN7FMdSjp09kNMaHnLWKuGX0xkwLxfqfpOUaUg5u2DKWAAyGwGG6KWkTc+4hkA0GI npSpFAX6FFFSERWdRphdzjPWvIrr6umIj3/5CR9//IT94U7KUslAg/nfPJxN20Nk5wzs233k0jxf Ja3kgJrv292kIh/W/ilESKppX1GwMAMUECj6GgdkvXtOI66gn3RNMtsbyKW4LpVAz+Stdu5xoISS 9KJno9V3uUEodONjKuqmLnYvqNaj9UipPdz2hiv2m6vsAMPPU6+GNxDS9S3XUkMB1mmWcN1fRFIx Hc9I8yqqEl1nBhYEOIjUggnIjRcHQ+anAQSRXqjLLRc3mDWaKd5CjExFpRemMlOpfslirMuiEhyY wYWxphPO84LlOGH46ZN87g8I4wiCeaPoNoH6Nm8PBoR3Vecc6RdmTOczXh4ekZYFEYxcxFsr2Hwn qKH099PwToAAYKhucnWAu8RchmS2ovqmIFZC6gS2ed4IbPtpwQOougmBevuOrUis6jHZdbyNbSdI iWnSzgpFIgeKXQLEFiMECE/lJpV6URAiO87ibmHSupIKppcTcs44nU4oa8aHH3/QhEtANpc7kklI Smz7vrK2qjrJr3O3cGQiafstap7vNqqum4gRkUBYkNOMsh5RlglczmCNUWELnBvQQLplsCRUxgSr Pp5B1/KCWBt8Zdvpdgvz+oR8lxSsuX7LFe3N5y/ffL0+N4rZmqK5V5A11fanDJh0womqEeoOn+v9 JDErQhxVPD7A3FJdC22eJYXAhbCkjPOUcDqMcg5BAAAgAElEQVQuOJ1mLEsCcwCwAxOw5IisRlnD 3Sf87f/8H/j8z684HV+wLAzmKFkOIZgggFEoW8VUhKtqRGQwZe8t8RqQXbAxCjGW08zEgdQIOAoN UCNvc3s1+yj3PlCBmIOYoAScRCVnc49gibaASMH9+zkAlAikYad5KZjzGXnOWI8r+Fzw01+A3YcD OGorNFFbZ3HveKnaggnwon5dNsik2EZJn9fVUBmizacqXnCQ6LOmIhFYyG7R3wdf8wkMICJRwECS v6J6cWWQJoizyBLCuJR5oQKaAFubXE82c9nlO3WwGzCmdMHmBtf6O1CRTrPmuHS6ttHUbxu+4bUs Pgfbw99FrJtIyU1igQmL0iGL1yFBAUntjghlzTg/H/H09UEMNh+fxUuj1PGUIFiNjQQLmDDJReaq AnF1SNF7UO0w7D4G/K8B+qLrxpiTSUDMTE+8ndjtBgcGypyQ1yN4TsBxwfDTRww//gAcdlgDYQUj UZUw1IIaKT8zbOMrPLGAspTPa8J0PGE6n4GcJSonW587ceuAnVxpwKaNuwLJOsOrEbLVL/77v0qE TtfpVPzuc+7acSGyssbR9pm6o2knmDNTRzUtI+nfY5azDk8212vL9SvVXYZvFsi+szLtto3tb7Pw pkpEiupHS8G6LJjOk4R+jRHDOKgXC6uIzAxibCfANTgNM3LhTX8oPOO637exMJsI+a6uVDqIEQXE C0o+I8+PyPMjkI8IPCtYa3rbdhTafvcG0HNuLOtjhYvP5Vi0hLP25GvHe62+t/Ple54FroEL68n+ Q1fOGUDo3kftF6rgQr1AZK6o4aUZX5Lda2qIqN4VI0IcQcGicAZfDy4654B5znh5nvD47YjHxzPO 54SUCIUDmAcUjigYkDkAYUBBAIcBu/0Bu8MeNASklLAm2SkFrZvY1rD6vwdn/j43WhCqvVkpQstS W1Bs5KX5xY3KbdPXsoPvyNLlIGoMjU7J4bcpENDbeGXkOSPPCeu0ICEDIWAYRoAs+Bbcjc/bQXBb LmNyQW1DyF+odSQbS+oliE18iuDPBWxXiWATpeSKRIxOSdgQEaUXBGQEJAooFEWs7eXYTNIZ7eJr W6Bm6yMeEuaFQ0pf/LZmJjuYcDLKBpWk/6m238EiUXd+QySEmeqzbRvNc8b+tjzNuwdNMaAaBRN1 c2ZEnQSfIjABmXF+OePxyzd8/eUzHv75BceHZ6Rp8ZdyaaJrQnbtmeVj7zAwkf27GnOySjCgaiwD Fmx2FhVAuIGnjpFIOar0p+VJDnjVaSsURlgzMK/I04y0Ss4rjiRqP+U1xZL6kfH6SvVKO+lKQQQw FEI6nvH46284fntASFnexZCQ9M0Cq6o9G4sKN1q+Qmh5h62Bes8GXFgBGzRpBVwh8NtrslBvMwPp iD4+RX9vJSa3jIFq9TY7zO5pqhPYCIQhrq5cRkst6m5FxLvcFCoqAMY0nTFNM5ZlAQMY4ohhiELA oSJwX7D61xffZd+osNx3AP4xUmWROUmyysYA8efgFWk9oSxHCYKFFTEwNt325nGNkW/H5vpY9a34 3vf8Vx6XkOHK58aFhn7VMeSAGqJZvDuIohK8qGMTVYtohZmxZgBhgIEPBJ0rniREXlZyxrokTNOK 5+cZX7484vOXJ7wcF+Qi4EUywggTKrYjB0FE6QExRNzdHXC4OyCEiJwK0porYbNGXR1LWy3ULiS9 IttatkJaPk2NiB0Q6g3r4DrutrMTRmVEua5Ju+6AGK3Eoe4QjWl72czadyvO5xPO0xk5ZwxxwBAH iVJYa+fgsYLletHCal8CGus3pw4XU8fHkmj74BtHy6AreK25OYReSnJZaqI51n61viCj3tvXExwI 2UMyZJU+VShYPS68JZvuaM8Ttvdte6WpTFMH4jqS1Eh6ghq/ZmX4bg8G8oCCBGCgiMiEdVrw/O0J v/3nP/HbL//E89dHTOr2Kbv6oMDCmL9JI1R1qPPMrrnkwsCDepA48OB6f5VamO1C87uRlvnc9/Fo nB8KunlOupGdlwXzJOnuuWQEEMYQJA8KNuvXNsO2dHVSEwNDCIiFcXx8xJdffsH6cvJcIgJC68ba oaWus2Dl2IgSOfhwZKPPtvMEuAIubHpvj+8GGDfutYpsn+3KMyLXAZ52ilL35/ob6qC23FZpoXw3 1Ij+d9CQqaTPGjGxXQ6zhg8/T5jOZ3ABhjhgN+4Qg3mhKICKof4OwDCOqtNq2ssVKRrTa3uyJiAy txeGJKVJQF7AaQZ4BVFWacv3M/HXGP8f9X55zzu2970OZN54/p2f197vbfYddoCF6Q40VEChH/Ho EAkGse0txfPD8oQEBRXialp3HDGImH+eJzw/HfHlyxMeHk94fp5wnhJSiSiIyAjIrJ4HBGUkFSwL UZD6xxBx2B+wGyW75bKuSNlsGMjf7aSB+zVnTJ+MYnAPwnUlbYA6bdYUOd+s4IT1sYbQGpLZrEtu CFd/fzM09WUAi/fXMi+YpgllWTGEiN04IoZWkqCqQltsajtVtJ6VeFrx9anX51Rdr78bSDdtcXZl zMrG/AJY9LPaDEmd0dg9nUiz7qI7yQNUpbwBFlfXzxVg4bTSPKW037ZAxVVlqOPiT0eppwZiFTs4 BRUokgwuMFCmhPPzEQ+fv+HLL7/i4ctXzMczSsoi0eKqWvEEeICAURh4KP6eCjxKBRClqEqkNJKM Oh/tmWoY2gCPBhRbVFlfN9AlpbYcPlAk7v5FPfyZC9KckKYFvCREBnZxkLw7Wh8QRC0fZF6Lt7jS UNZQA2vG4+cvePz1N2BeHVxomkGpz4at+ui0G0yfJT3v3E5f4Bq4oO8HF9111ApcgI5G1Ppa+XTj /EUD6G3G19bHz/HmuRZ8cPsEd3/sbCQSFUiWybfOC+bzJLqtOCIM0d10LVa/SUTMBgLNOhcRpyJy sleyS0tM/A6obrxkmRRlQSkTSp4BJJAEGECNhfrHj9e8ib73+B5w8Vp9/rgE5C2wVAmAhDoX9QWZ 2iOMovMlMcQ0FUklpk1wLLWpkGRC0csAxD4HDAwhIlDAuiw4nmY8fDvheE5YVgA0gsKIVAJSsYiP SrhDQ7y12qZbZiLEYcTd4Q77wx4I1XbB+Tg1jJNlDsKBlXIDkxQ4SLhyGMew+6wyxrhgYKIyRNcZ V2wk5xpDmKuj7OBEyyxFY2/Y60gI9pqxzqvGeEkICBiiADzoWuKmH1yMz80i7FgedJS5ajbAdWdn RL6RQPxegKxL3u0ylKfWGAtkACL4uyrJ4q7jWK+xDowDD5B/dxUOU6fm+b2HgIaNtIhR6+r92kEK GABiZ5aaF6cU2YGTuJdSKphfzvj22xd8/s9f8fDrV5wen5HOK1AYAQ2gKAXZwIGGKageHBU0ZDXO NDDhthbNvW7Q2QCIavgJtGqVNjW9XaseYKRggzuVhriBwlVHBIgjQilAKuA1oSwLOCUJlEVqa+JS J1RVlI4DaWyTNM349p+/4vj1AUNmxFIQ2YyEuZN4mPrN1WqNtOIWuCC+nDN/SHKxJfQtwNje24KX 7dG6nfaX+92Uie5acPHacc0eA8CFwWVzpSvW1TfNxwkzi1FRpIC0ZMznM9Z5RUorwhAx7kbsxx2I RbwnjCWIBfwFQlSWpMTcdxRogYWIkgkFUaUWnGdRi6QZxBqEtuam/lOOrYrkjzD2PwNc/DmqlbfA BWAchzXuhAACAxcWAEskFOwDppRBUiL5DpZU6iFGv9GZrEVjMA29vGcHYIdSBqRMkEBaEalApBYg pJKbWlJDACzCJnytIASMuz0Od3eIuxEgYElJAsI5cGhrgB5coGeYcP6vQIE6yKDtkd83XT0NpLRb JQPPl6Pgo2XXfBMI2/mZTl5uDhQQIWHRl2nC+TRhXVZZTSFgHEaJo9EmtTJSYX2yqYODC1Ym0IAJ kSZsGvgH2LNLVXTXq2GQNKOm2QygSl60DibtaNm3tYBJPFyKMZKGiBKRG8D7U39gmV1s5xzJdnDD Z5zlvzA6J/NW72FWYCH5oPK84vnrI77+8hu+/vMzHr9+w3KcUFY3phBenAtSzsilXICBXjXSq0WY LTlZ+4xJJAzgoUopsFGDAHCri24DWxzIAQaMWwuXCpDFhbWorR9rgjUAuSDPi+TCmiQA1hBiTa4Z qq0LitCVCIBSxvT0jG+//Ir1eEQsRWKpmPi0ARdQSYbnXwFc4GD8r6WftPlrbQMacCGFEJrhvZw0 DcDoPUIaEIH3MKC+/A6wXL7VP63+yq9xS5JeYxp08dMXwba+tRe7k+1dgSShV85ZA9QE5KXgeHwB gzHEgN0gImlWMFMNyvryWmJgPsm26MW9tappAjGGyAASSpqQlhO4rCCSwFgyvXtw8b3g4M8AErfK /e84LtRMm89bhJPNjcilDpKplNTbQ5LlqQSCLGCVaS4thXolps68g4a/VgZmUi1Z2SLZ2I/3uDv8 iCEckDJjXTNShoazF2CylrUSyVJcUmEmwFm9jBiElMUlbtjtsNvvMe73SKvkAikqU/WIhtC1TdzQ AuuUBtazsb3L9WY+8s0Zv5f9Ol0sOdbIlOZaaWV3ahZXlxQB4qTlaH2DbvuCREGCeWyklHA+T5iX CSlnxCHC01KHBkr4e61cmzNNXzRoh1oQ1zZo07jvn/cG7lqDSHFLbv/6frfZrfiuk7n6IFHdDVfD du9hBDCigyb2sn7v0VJzp9ldeXVMyfu4edb4DMsnIgCp4Pz0gq+/fhZpxedvmI4T8pIkjp9wZDAD uTBKG/LPQEBpDDdRDTXNKLOTVlyAEJM+wO00qmtqBeS9vUVtsYRgN4mbzGGGAXW2igrvtb8QyVgw SVvRBJ1rQl4WYE1AlmRkFKKr4M3rSagVoUwLnj9/xfPnL+B5wVh0vP191vlmC9kYDRsd4Dqe2zGT caugyY4OXGwHvz3eqxYB3p6XVRDRMzAzaulLapvRTvyKrC62NlfqJJepu5naexrJCnfx971y/qfV EobWJrswcs5Y1wXn41l8iQeRYEhAoNK8m9CKgI0AAHXwnHBQAEiAhYhiE7jMAi7WCYSEGOWaRD1t A/b8uQz9j6gl/tvAxVuSiYtqVNImbtXm9aEeFhgUIEjGUgnJLWAjBpFkePp0Y0cdQAsVbBAcBIAJ AdUQlDmCMGCIewxhBy7AsqjHh86FtSQneLlkrDmrLtl24i2zMxGpGAciRIy7HfaHA+IwwHIoiGrB 4qBcjlE1rgRM/29UctvTps6zC04YuRLQ4POzNeKUZ4x4VYBhJoZdhWDqxd42BB7GgwuUUUphpRSs 64rT6YR1mdWVPaqqhGqchIZytkB0S0m0cVLDhjPyH57j1ZXd20Rq4EkmgbBon/r+0Nc5MhDNI64j of14mWIk+Ixk2wndlDq953BqfRVYSD3svgsQC4f06gVSML+c8PDbV/z2n//El39+xunpBWlewUml FVwbmVsVBwBWQFHUk68DBXo+owER/mlsMIoBCjTABF4WN/O4++7rgOvc1lMGsr2/KupQUGVSBR03 /R5YVEOhALxmrPOCtCzuhRhjkA9EJRJzwXo84dt//oLTwyNiyhiYXSXiAIOqpDGwgguq40I+nqoE 3mgZCE119Yj/99//7R/AFhz0Uon3AIr296WVw/Vnb5YrN9y6oofpTBtXOadR2oJNa2sH0eb3lsBV CKL0VN5i402tDUVAnQfSppwS1mnGfJ6wLCsiEcZhRAwR1rRg4cstvLmKtgpKlTuQGREKEAkAAmVE KkjrCXk5AbwAlGEOjYwigYaafm6P12xUXpNYXFNxvffZW/e+dVx4C70BcL1stOVXwuNucw4ATIBv Pa7qCxIDTDHErOG5gUHOhxExDKA4iEtpHBGCRNks6inhYIJFYhGj6PpL0fFhkTRY/pCg0pF1yZhP YiU+TzOyRJiS+C1ckDiDorijpVKQS8GaClIqyCxtITNQNALYgAwAGMcRd3d3OOwPAJFkbMy51tvi Txh4sPnOVaIh/dbKJLYD4f81A1ov1l2gjnMDJvw5/dkDECWETrwbJGO7PvbA9l0bwEBKSQw+z2es ywICqUcJud0JNczVGV/DpAnGMHtXSyjzDxtJYWdP9o5537rz2biJLj040DAxtu2aGeaCKO7qlunS JCvGGMSwVeCmroJuk2Nzx8bV+05Bl5/n/h7uOqsh395cGVcDjgQgWCwSy2lBwjyJAcoFaVrx+OUB v/5/v+DzL7/h5dsTlvMEXjXIW0ajFiO3sRBg0RhkGkjQWmSubqjJ7mfWv+13ttiDXRkWOMuNOX0M qorFW20gvD23GW+Zy8WZjczDavRf1Vjq3cECAMAMThIeYZlnpHUBZwl/P4SAoQBhzTh++YaHX35D Pp4EWJiXiK8dA6GNspcr4PShJNS53QxvBeZ9uy7UIsAluOg6anPuunQAV6/X79eZRnPiQqJgjNue 98lsDfTrdUF1XifNK0lnuCHn9pBFSkpEyN/viwwVWNj3+l4AOmhm9DmfJ8zThFIyhmHAbhwlMZM+ JwZmdZESkYsmiYyoiL1FABBDQQwFaX5BWc8aLCsDlKshD26Pz2vHe20d3gs2f897tvddAy1v1rNh Yj1XaIAFLIdHrCDOPmxLbABYJRjq8WESjRBHhLCDuaAGVZHAAp1RaytTPy2wIBpA+g7mgHUpOL2c cHw54vxywjTNMp4hQlz5s++sUjaphcROWVNBMstzbSZrCvTixCP42hrigN1uxDCOAohzRioZWe+t jINhBqkA+nxA7XIm+2mSrWo8arf2IKSCeWzoynZ4WxWMD28XEbYdcoJYQwpTrvfLZcs7sS6LqEqm M0oq4lUyjBhVZVLBhYINqwvXxgqGof79zczrfr8TfHsoDGqsJ6ia2JmBJqgCi0IAE4trMqw/1diQ TF0m9DByszNlc2s1oMIqBan0zGlSo9K1uVGlcc3HqY/OuYY5eb+QgBjx/hApsbgNBxBLSOzz0xFf f/2CX//ff+Lh8zespxnIRZLiFXbDZbO/sXgVNbCVAAALft+lTi/qEdIBiyZGBZu0os6tCiS4BuFq /zVgy8a7l+6R94f3oYMOh3HabdXUtr3qKhTNBWXzpZSCvC7I84K8LGCNNr2jAEwzHv75G56/fEFY E0bNvhtR3yN8UMEd0EuyWizQfOq5za/m53eBi2vnrl6/ATCcCTsB2l67dj/qJL52NIDC0TXXAbSu uC1NuTzfB3i5wJmOIrvv9npuA9wQOGcs84zpPKOULD73JL7cQxRgYZHhbP6Zrgsg33WCRC0SqSDw ijSfgDQJsEAGGh3j1u7lz2D638Xc/8B77PprnzfL3gzZ1gJDJBimxmjygKjUgosCDBoEAFAEhQEh iKQCUIkFmUGn2mAggmhwqQEX0kA3weMzxDjAInYSjQAPWNaC83nB8/MJz0+SNyStSXdXaHTsMsnW XAQI5KziW3VRZQs6JNl7WT0j2PpgC9ICYRgG7Hd7xHFAAWPNooahQAJquN5LoH6D4YDCFI5XxmZL kbj57l/b3ZFRH+WyaD+NJ4sjFyO87HUwieZmX928U/6VpOD/PKGsGQOJRwmphBEW4RIQcOWc0ewc nI1eNPlCRry958Zcbu072m89MO4lBaxVLZDU7ggEDkAmkXYQNFoxE2IBBiaPcSBNkvvN/RFU9ez+ xooVGsBIzpB8PLjyD3m+/mt3vcTw1OBDEMP4vCZMxzOevjzgyy+f8eWX3/Dy+IKyZrWpgEgSNN2D BbXKzEho41P0kgVTceRSkPMV6YRKwapkogHXjGpjcWU2VTVIu0HUa600vAUVXrZKLDaTyCVHBkzq C3Tsq4TESqXCoJTB84p1mpDSAkoZy9MRT7/+iuXpBXuWTKVUsvIYc6SofCdQVYmYoa1Nte6v1eWV eX4FXLwdJeGWOsR/XymhX0x8cW1bzi1gUcupC1/m/rVGEsyoi9p7nAtTgwj7w84zd2/qrtf/GzCm i6skETPHKJYhaU1YphnTWfywxzhiHAfRBZaCGCMCEUpOGMgCNkH+stQ/EiMiA3lGXl/AeXFwYd3E QKPT7vvyrePPABd/9D2v3XdNcnb1mc1CbSN0soEImPrDImia7UxE4QDCCKKIQANCHERtEQQ81JTp 5kEyAhyRMyGlgnleMU0Z61oABMRhJ2oTlnD1gCSHYw5YloznpyMeH15wPi1YloScs+7KSC3XSfJu BANAJhrXNqnHihmirmlFthwGEJCgFpBOSClYe4FhN+Bwd4dhHAEKSCW5C5+L1pUI29wycCE/qiD/ qnSijk5lut2isnVZ1/W11ex0rF2THceT8kX8XdSVk7sCCPCYA4EkfkJaEqbTGcs8I6/JE0ZFdREG yMFhCLExpuxaVjHUG8Cie24L+NASdevjCjBsxP0Jd1uxLQYhoyATNNdScJwWQRjDIHEPfOcNj/Fh Bp/EkoeiCSbqH6+jQx+qbVbaJ5GEVTJh53WwDC+aPJAyI80rTs9HfPvyDd9++4Iv//yCpy8PWE6L 3hvAqSAnAc0mgbCQ3GYzkdkMnKtar5VKmCqki8ZZipaHGgBL6T639/n5CjBcJdX0pQ3MVjXSg4tW 4sF1+urzDAU43t9qaGv1IgOTqk5isaGIuYBSQVpXzNMZ8/GI+fEJy/MzaFkxKnCQrNiNzQezAwt1 H2go5iXAcLBzfUb7t6uSC9Otvna8BjBugQv5e7u8WsblvS0sqaevN/AS7FhZFf115fK2/nKu7eC2 DoDGm2h+s468iwu1Hh5dkwtKzkjLivl8RskJgQi7YcA4DuKvDEi4VnVWMB0rgUBBwQVlcJqR1yNQ VgTKIBQhCkSwQDtv9cl7+u3Wtf8ucPF7nvedb0uUETYeIKYKiZvrco0wIoRRgAWNAirMcJOtDAug NYIxYF0LpvOK03nF4+MRp+OCeU5ImRDDgHHYI4RBd10BKTHWOeF8nvH0dMTpNGFNBYXNSFA8Q1Im 5AKAgoi/GQJ2QvS2isRCVgaDVcVRxN1U+4TMXQ3QnDxFXVoJIQbEYUAcB+zv7zAMo0QInJfeQJhN SnCxgpxJwX/1R7Wr0GGoWKC9qz5bSdGmOHbCwJtHrGxmBisXMwxkRJK0/RZnQAAEISWJizHPM5Z1 BTOwH/fiTk5Bd8PwPuzmnM2i92OKi6PbNFWCpesf2o7qRQC/xdQ21ZMksUm7JERStuiUuSCnos8I WA1R5rExv8ASoCo2wMLbaKpk9DTRaSgMmEDDWWt/678IMdKkAiAXpHnF8ekFD799xedfP+Pbr5/x 9PUR04vki6GiUhp1Lc25qj16FQhq+oVmQrXAgltJhbbWU6wbfzcwUXqphwEMx0oNHjAAA/RTkZsy 0Z4Hb6dsc3AHLqTxOtZNwVVaIm1CY0MhtjZAzgnr6QQ+zxhSxqhjGoqlXZP3EczTqoYBt3VSQb+N fcuFrx8tW7zIivreY+s18NaushefXH9Wq6fl1HcYWjQ9VoMJ61O2GK8gqgq0ePNUDVZiidAA1tTP ImI2XaPVI3ga3NZQi4XgBtbdpFjsAox1XRFJFnEMBM6M9XjGQy5Iy4z0t7/i488fsb+/Q4gBIUZA E4fZzqBHZIzCCVzMKIttUyOE6A8QuPce1hf/VUc7t7bj+n3eKjqOtYMgnaUaR/0txrqqL6SIGHYQ Jy4hwq6S8N1FBRg5M87zjNNxwvk0Y10zzpMZRzKmaUZeGPlTxN3dDjGMSMuCZVmxLgnrKjYM47hD yhDPkEKAGgqLkSapYRlLkKBAGEMEDxAmgQLmhJxZA2UJyFhLAS8LUinYpQH73Q7jOIK5JuRmIkmW VQowROyHe/ykXhQUIo7Pz8jrKv3e8n2nO9fWmzH2cLHuAWhgCaWezTon8n0wqAR/kSRII4QAT5zQ 5rrg5jmAwVRAhVBCkEiNVBSsAaWJnlkyI3HStRnABTgfz1jWFfN5QppWfPr5Ew4f7jEcRN2VS0Eb MagDFi2N+b34mWS8LfaGMwGu7yrel2Z4alIstZAZAjIBiwaPGgKwEgMpoZwnxMLYEWEMhHEMGIYB IWgsCSL0kpFmrTebz4tRbzcfzghrnTgzUkooKWM6T5jPZ0ynM47PLzi9HMV4eU2S+LFI5lxmFlDh Uji410cbl8LBBXNHJ0xawSqdqOHUm5gXpT4rf6v0wsqpzzS2NzAQUN9pkSxMwmH31cNXRsPe0ZQI nd+hnuH2qvKXXMElNeNRY20CIxghZVBiBC6KU6SNZHW1DZiubfd7ZMAT3HGryeBtg7qjBZ2MV8DF e4j4tXs6IIBLRlRdza4zEEPjzPasVNXKIWh+ge61r4EVm5Q6/0kmlnlrAMLKA+SmYAuWAITiu8Kg OtfCIuomIoRAYJa0794WTRxUGAhcEFl2hoHIo63FSOB5xdOXb5jOE87Hn/DTv/wVH364B40EDsIg On4mDZOJnjPAGcSSIdFRtAENXDL/PxsM/FcCjFtqsvcCCzNgc8NcBRZMrSzK0kzZuzRPSBgRsAMQ lXDYIFRdPpeCnFasacGyFJxOSTKWTitKkcRiCBGEhHVdkJYZywL88GGP/S6CS0ZOST1LJMjVMATM a0Y+z64KKUUt9zmI3tgyOzKDAjCGKG3diXRhWTLWUjQro5C6NWcJJrQmdzcFFwxDRBwH5JyRS1Zv JaDkgrgb8ePPP2O33+Ph6zc8fvuG8+kMQJMlOeHVbEtsZLPhuGjXuo6fzVKLgluy9DmCxtYIYiTJ ChAMQMiSazIhZ71WXXw7aR0BJh5hMJiiSH2K2C2VXBDjAIpRd6lF1qjJBaaMl/SM6TTh8eEBP/3t L/jpX/6C/f0eRGIxz02LbaduIKBoACi8PVWv0irzBrHy3Gq/ARh2tJ5ARjMK4CqDSAQeIkoYwCCk UnB+eEA6nhBzxp4Ih2HA3TDgEEcMMSKOATSQ2t1YILi6lnwjiXanbpIDAEU2UTllSZ63iERoniYs 04xlWrCcZ3ej5Fzc/TKyALisGwKxl82/1IMAACAASURBVKgGlJZQDA4uZE6b/YKDDa6eIq3NhAfK gkglcnPdpAEmnfB2YSO50Dnt9/u4QZNXNh3TSdmadxgda6T15lkCBsyo2N9lt2s/EWsGcSg4IkIm tSVJBYEZOwa4ZHDOoFKVv1klZJabSKQX1VOnla1sU2X0x+sT/HdLLuy4BTCAtwDGdWYhhOjad3Yx cLjCYMyK/Vq59c3N7qbtLJIJXHcFMgjFgl0WoAR13SpCWAycCLETw73CLESGJDUyiDyFfM7iLkoE EEeUlFFywZQz0rpiOk/467+IFGO4Gz1CXfD6+/IAl4zClvzX2sLCPLnv8+/b6X/f8d8GMNiAJaHG CukPBnmugn5bqSsUJpmAL2Lbo5nHB2nWUsKguxTtP9stswCM02nG8Tjj5ThjXRmFI7hEMA9K5Efh mxwBGrFywsuzgI9xF3C3l0y64ikUQAEIMWAfBETwWpCXgpLrbsmAhUgDCjgDUL3pLmpQKARQSli5 oBR1e1ViseYEngqWZcH93Z26HFYCp8JywJgJBdyFj4hxxDCM+Pr5C84vR2dY0ulK+YwQteOhzHFz 1v/4FbYdVPALnWySGXDJhahzXFqhgASkY6/zPajHA7F4UICMgDI4RIQ4YM0JKJLZmGJQ3Ttr6m4G ErCWBeu6SgKpZcbPf/sZdx8/II4D0NhzWAjlWu33AQub38KgGia0kVSKHUT9bQaS0nX2rPQoK5MK FIBAKFywMCNGRjjsEIaI/RBRhojT1294fHxGXFYcmHCgiJFIcnuMQBwihnEUe7AYNMdHY3Oj7y1F o2HmjJIKSmIFFUncnNeEtGaktKKsSWKrZItRYYHCARSTvKkNiEbYdHDAxY2V5fZqX4Fi4KK4PYaB DBup3t7CAIiBC/h9dTPRqzDMU6gaUnaj1M1ZA9Q2vzsQpu+v9oQKhP2m4uHSxchWx5jhbsZBGqfl i3SvBCBBwn0L4GWgFAcjrHUJVCX20NmzNQHgctnGrr0bHn/tGHzSki2S1x+w6+9hKteZm3VkPd+W JeNRmu8tPtZyL5pNF3WqojyRIklOAFKJg4jLSJE4qTgQtmjto0SLENwgSuVLYC6w/GOGIGVHWa3K SQFHghD7GEj1vRlFi6IM5POCY3oE1oLpeMJf/o+/Id7tEQaZfJkLQmTEAcjLijWvIrYiyzMAHz+Z /4Zs0SyWin6vHe+RUtl9fxRQsFXkKhhm36XJqhSDxZaByW60masaKwIWclt3NcQQ6VKziwUDZB7v ep40kFWEqEMK225Y31gIpQDLnDDPCY8PL3g5TjidEzIHxGGPcbdDiKPu3OC7FEtwlkvGtGRMa8Ky Mg77HQ77QdVnQpgBwm4fgRgQY8FECfOSqx2GzsvMEg/FCCIoIARgHAMojiBVY6wNs2UC1lIwpYQM YFckuNswqFGrhqUPFNXAr4CGEbv7gJ+GCBpHfPv8GS9PT963IBYmQZJvx5h7GwUSBjJ0ndULrMCO GjE/lEeX2vcNaJQcImZTQ1pXlY6E4ACjcFVnBSXOAHsQOjQ7QgRGYI2gWkikHAx4ls6VMD+d8G3J SOcVH3/6hE8//4jdYYc4RPF61ci4HGq6gH7hNXKdQM5E5BYZU4s6Ic+zTXSpA5OMB2q9TPVlr7G1 zgZGCCBttzBcgAMQxoDxx4+Ihx2GwwHH3TdMXx7w+HzEcVmwY8bADLB4DMVhkBwfobrx6kJ1cCgS ALHzkZxLRUBGriGsUQzUAzVUpfzMKBqvQm0mSNZbKVnVIFCgUKoEAVKe5/GwtrPZS5jUQs5bRFoL rgVUwO70zDZpznZVTYK6GbX3NtDS6UodD97M/5bUVbjCDN2MNoSQ2zJlTsr8loIkmq5Ixmsd5d7W 9oKUBrnaPBhgYMfzwWelwVO9lxuehp4/9wdXMmqHz1sg/sff/+c/WsQCbG6+clxjRO25VrLQAwfH RW+UWdUe1+pCzfVrZVyI1Y3Z27tb4lcf9nf75CV/2KUmfo+tapt0TTsFyFQL37oe6+7Es9Yp8SQG eM2YphkpiU1FjCP2u53omlEQKSGtJ6TlJPEtSOzDO0IE2wm0ujLq/tw6/qjR57uPNqThxUcBXyuF EKqqI1NAQRa8gGEBFSJViOrRETVzqbkVBk8SFoOJ56HMTj1DMCDQDoSg+m5ZkVQAzsAyJTx+e8GX 3x5wfJlxPieklZAKYVmBVXPGZQbEdlKNfglC1dWAlFkMNLO6jdo8MoJFJMGFhiECKEhZbCkEVJg3 gBJbstlq/QZ3fTWQbgBfYiGIaigVierpWQQsu6vuykQ4EESKEQKGYcTucMAwCvCROiWRnhFp3BYV 24cKzru5wkD1MfDZBKCRoKBuSGweXNAKazMrDbB70UxvnV8ewbJ7nzEBrvOqCeLUMRrbABUgrxlp WrBMC0qSUOUxDgIwSBlkADhWOmHf7K0mcXCu6nc5GnBQK+7ser/fa6Jy8r/tmqkdIewUrKG9ayXE boQADBHDYY/xcMCwE2PjUhjrIpEvQ2FQBspakJaEvGTkNWM6TVimFWlOWM8LlvMqao55xbpk5LUg L1miZ+aiOhqYbkPjUxSf7wUazAoFyUAAb+5hvdOAA9dEZGbYmaF/vdz6t/UO8URjCoqqtIAVBLbB svgSWJAx6HrOntder/PU+pydo2ynck+Tub2npeGtlYb8LqqSZPcDZu9jARdt47TeDQC1sn0GtQCC Gh6K/vt2vVXWRs13+Rv/4+/Xw3+/13Pg2rmWgFz3MrgEHLe8T66/6/Kea8Bie87Fb82ut7sPm+HX waAW3fr7e4BTy2G4SMPKbAcZho3VXYts0ADZBRRM04RpFl3kfr+THSkJuMjrhLKeELCCkGFat67C N/rrTdSI6/39PdffdVzDFD5T1ZuD1btDTMthWV+FtSozKxKIKtAAi1thVvAwuI4gkgj38BjqNbb8 IJbxVEXJhuB1jLgQlnnFy/MRz09HLCkjJYbFu8iZsabkOyXSuLnSVwYsCKAIiuIFUgqQ0oqc1O4g qFGhdm+IEaUA85KwpIJcpOWFguuHTWrl/FCZTQhR5h2pMs3c85ow5BJ4K7k3SIyaD8Ulfmq3AugO NmIcd7i7O0iYe7Ubkd0RJMhig6S5YsKeCPmOq58PCq0cKgnhvT7XbOfVklu5nbzOdo+/QCk5QVz4 XJXI7MTWfhMYUO+QoEBXQvsXpDXhfD5jnhcAoh6NQ6zeO8YgjB60a69h8PWctpYq9Gj7reuk7m9/ OHGnyjwC1B6k2eAIKGEUIlAMGMcdPtzf43A4gBR4yqdUkM3Vy4NTccFDp1pQf04DD527pwKB3ghT VREaTVO8Pap0EVZ2G+SqlPrxoFn6vTlvnj0OTBxM2HdbN1UVwU29GKZOqbS7Nez0eYgqhbJ5aZls W1J8c+ffbVjbe6mfS5uX1LpuyjdAXN1fLuregvem0HdLo1ue2X6uPX8FXNRZ/T2M5HKXchswXAMX t+5/DVzcev7qOdTFt32/Lcx2InXv4FoOQ0MGNPXYoru2xj4JNgPt775GLEhsNOZpQkoruCSMEdhF RkkGLhIIBUS60m0XeoP4tIT/reN7geWfewQwBjW+LABlEFaAMiyxDnMAcUQgiZIZVEJBtqDDILv8 QvAEY5Dw3OA2nLcw/kBBPBJUVy/jpX74ISCGQUTwTAhhwJoy1jVLHAi1d7CdUCks4lwjEqHG0ABs 5vUiR1edBgLFiJwKKETkApzOC6YlyWirO2ohDXgEmMRZNz11F0xEqMkBqghd5nZ1LRWinF0cbWHo bR6ZdT4BCCFiHAbsdnsMwyD6/GUVlQ5Z3xUHOSq/6LCjGIFa+1uCxLgkebWP+gvtLuuK7VbDPPrD xqB5l6KQ7l5GR4RdqqOXl3XBssyYlwU5Jcm0Oo4YQqitpmYu2as7lFXrc4mxNzvc71xzLUDr6JH+ kHgWcjYQuW3FuBuxv7tDGQhTTpjSKiomEFjtJMxYOhNLhlYzCmS4Wp3BYlyIuplyKQD1nh0GRMz2 wWKQOKjoAEUN780N8OiBg6oyHPRYeU3gKTZzhUqTgeaeWzS0nWI+Xypjbdi9w9XL+3tecp2l9+/f ggcDTtt7qi1ZLaEF0deOq+ffCTTeOq5KLkQ8e8mo3zo68cnmXP19/Zlrvy+kEV42X1x/T33rlf75 zem+ns0OwiUTyjgswRmwIUZb4tC+oAle0razkYqqpkNzlCwTpukI5AVDLADPAM8IlBBIQtYJIbPQ ztU+oWtj19bXx/R/KbggaHwPAU6ijRZQERBBHEE8ItAOgfYAiRGli06JwPGAzCMKBjAkoqYkBbOQ 2wQJ520eB9HjQEgVTF0F2bVRQAwj9uMed3cfEELEqsGyUubqky63Y10TUs6iRwZArE7D6hooSUsJ CBJfICexkK9ETUDJvBa8nCYBFxpQay3ciPvNq0CApYCiZvRVImIW/76ZIVIwpu5+STxKmOtum0B1 znDzHBGGccThsMe426NAnncxbSN92G4yekLWqCYMalSO39zGfuqCWOuzjUC2Pn4LqVjPefCpdovZ VQKtbVBXdSJRIawrpnlyN0oiUSGZ1EfhlO5kdW0SuXi6pWqeyr1pxu8+FFNWT5OKZQqZZI6g8bVk 7RAhjgN293eI9wfwfkACFEivGm46VHsGaOCpsqF/yvQz2Hhhp8pgTyp2DRTYOTPK7CUeWa/Z8G6B Rc1ISv5uB986tCJtoCp5aa6Z9salb0BVbbTTpNkgsgHZBrx34HVzbJl5w2aunG3e187La/NDO6UC S2vZDQDh53nzewNIN8etDf+14yq4uCUZeOt4D7h4T/nXJQ+Xz75Wv6tlOFDomW9Fhu39zfObutOV etTnKiXaolWjOZHClT4SVEHMQIaKjyWORVompPWM3cDYDQzCimpvoXa/XoZ4E3Tt9sVSd2KvHf8r wYUQ4gKEBLMnCQoGiHcA70DYgWgHplFtEDRTZCBw2GEpBxxnwnkuSIkkD0gcNaGbeh055iKXPpBY 5wlTsciuAESZTgg0IA4jQBFJk4UlJbAMgKJ4gaRml5VLDdIDalzrwP7diFtmRsrqicSE85JwPM9Y EqNQRAFBe8X1wpUl1Y+LdqHeSSEqwLAYLexItiX+zKLuINXtk4ITmGTPP2Lot9/vsd/vEUJEKhlr lsieFZhd2a1xXR8dAun2i5XIyXpqrmylFPUNmz9XFqozBeM81veNDv2iztpHOlCiNqkENifx9JrO Z7E1YEaIUTwsghqKU12f5s2hM8aBkbuaep0vNyDb37dpX50NphaxvqiZVO13w0QjgQMh3h+w/+ED 9h8+IA4DUilYU0LSwGsezbL0EgduJQzOrNtAVsVtINo55xILLc+ARbYgV9jc779LV4arC7mq2Gx8 dfirhNGABTdKsRZEEvl72jKACiRgIIYu51F7GPi4dWyxcB3X28bzG6xcfzA675Cu3u+USLwGLr7n uAQXxiD953sWdn/v94CL2/dUxri9ur09+AKl7obeDqReJ7ryTt6e4+7Z7fs7gNTWWWfptYGsEopG nwYnOT67xziKqLoUDAOBuACU8OFuwOE+AFgBTVZmWNWCd4nLkgT6qTSqgVRs9WBctqxpx43jPeDi ll5uSxCvTXXxgllVaiEqhcADCCOAEUwjRFoRkSkgUxDGSyMyj5jKHg/HgG/PGQ9PE5aVgTBiGAeQ GrqFaFbURUJAhwFBQ3o7kNCYgkHtP1IqWOaMZU14OZ4xL0lASIhOrIQgyTkGBFhk8Soypgwl7rmw GGqy7WjFNiSp3jlnYFoSzsuKAm1jIHAISGzBtdTaxoP7BN+vuOhYd6lEaheg9bVskoBJb3Q3njKq K0XdbcMIus03aDCv/Q67/R4Ug7gfplR3Y2ws9MY8aa847eB+cTVl4UZZrEDcn+/mVhW/+5JsCbKv Q1l7pPe3EsbWbsSMXqG0MICAwhLef14wnU4yEqSum0FDzfsabEAFte1pVLFbArPtt7fWoF7eSi3s U41CdZdN5K7JBQCHgDAMuLv/gLsfPoCGAUvJmPOKlCtAMH0el8rIbM4V79JG/bEBFqaaKFpGMUPj IsnFMm/v70FGBRhV1VI9QJp3o77HAmU5GGhmi8EM+78P722gop9frACdbd7YtY6V3JZIddKChtdt ZzFtv7D7NDY1qzzERTOOoCsXcJjb4HHaFP+ng4vaOPbfdlzTGW0PP9+AkN8LLrw+7wAXliBHf8ny 3b6fyHcGtzY+1N7fvLW/XQfTOHV7ZUMAtwy18zdRokZEasPBdkGYGwFEjEAFJa8YB8bHT3vsDxFQ GwSmgkJFp1iob2CZSdu22DSsfXNdgvFngIv3HFdxtHpZSAkBxAPIAIVlHqWAHAaUEJFpROI95jLi 5UT4+sz48kx4mYB5AdYkGUNLycIMR4kr4c5/FNQAUm0wOIDYGIK4qJbCmKeE55czXp6POJ0WrKmA 1JBUvO1kiSZVW5i+OFsMABZ3uwwIkAA17mQ2fgIOhdAWLJmxZkZmCemctdYcKlkxe4v+I3KNUioY MPsSCsG2rSpVkUVh8QuYGSUVSfWugEnsN8xIVj8BTqTjMGI87BGGAUSEdU3OzFl3zDbgfI02GOX1 n1Tfu5lXLY2p+MDo1WZedYRYf9pFpSsO+NGsfzJmAQ88ZnOlfb8BdfHsADgXpDXjfDphWWeZQerm HjwvjNGq2vaO7hB0rK7Rzds0tb9x83VzO3Ntdz3ZVMYCzoWAsBsw3h2w+3CPsNthKUlsTUpWXMbV iBON3ZFtsIoZFFepRusCWlUmjXSDZZ10gALc3V+lIq3xpQ2LSYgqyLB1Uu1AKj1su8A+Emq/Skqu 0arK3jd0vZ3z22FpTvc2fm+Pbd0USz/1rWjKYqinIhvy7upgwKKdd9Rdv328JWBoj/jv/+Pf/tE1 SlfgLWBh9762I7VF15XbPd+XdXm9BxWX9xixkHsugmrdAitOWC7LvHziCmrUX9RNw+1L4ISv0rYW dNRnqTun0IAh/sokv0tJYF4BrBh3wMdP92AsoMjgIC5YILVMYFIXV0Ol/Zj2/XJt8luG0NvjYn9f +7wlfpMdfCXUpus34h1UghAwIoadWKfLQNccsMMADntMacDzmfDwzHh4JjydBpzTDisPAEYUiA3E PEvae4AxxKA5XwzCKLgoqn4hyYBaMmGeV7y8nPDydMbpNGGeElKGgAEWVQWp1wiYkHPB2mReZIjz 0JoycgZSYfVVtZwOpvO23CZ1R5XVQDMjIBUgsaaQpsaSPhc3CLWsqDBi2s+s6kWhKpKagMvEvzIG zNUgbs1Zpo6K+qHASZhwULAaBGDs9jjcHRAYyCVrjg65brtU86SxXRZZHVtVHtcpazPV7qvErQHS NpttPlm0zDrh/A+jKL1o1nezwav3NR3WTedqpGrPsj5syb3WdcU6z1imGWlZEShgHDQYFbXAoQEY LfqpPy6O9xB27gDMxiKlAXDkK36zXjU8fgEjAaBxwO7DPQ4/3GN3dwBFycWyLDPWlLzQojZGEt9C xtcYdG6MK1upQgUQ5vXRAA1wd59F53RX1E6qgDrvuQEeDesvqFIG///ieVNjtpOvodzt/c25rv+3 48FNWfaF6rVrzzfKmvqRSiqA4FoGKm9zJekNElxV49evv3W8KQhorsd///sGXAC4saG9WfhbKobL Hcgb5b1yrXmBe3Fc3PcquKi2Dx0AUqJ2WYvLZX7bYBOVcBJVdY13fFN/qJubvrolJ3VzJIifkJHL AlDCp0/3CAOAyMiSFUJF82alLl4WjVldV7m+r/r6uSD1xsx8L2J97SAymUHb/8H/EonRJoq6mEIZ UwAwBEkjHUYsOeBlAh5eCr49FzweCadph7nswLRTJ11zRRWx9bokLMuKIQ4Y4oiodhiBJCEZMxDD CEYQtcR5xtPTC14eT5imFWll6WcmSJp1kUBQGNzFlEFYs7il+jrgID74pbrUASR5ZKjusAQgSnA3 U7lQEM+ZDEJCUcPPNrAPo+ZIUaNO9Wm3MfZdnP4FCQi1mBgu0iWqrIZETZJyQlZQENTwtbXxMJhM JO0ZxxF3+wNAwDIvWFJS/m8EWm1edPxtY9DZYoCMaleQwbiI+tkZUbebIS3D2Sq37AT9fQ1hr4xC xonYgI65HBqh39AGBRX6qIR8y0XiP0yzeNOweNsMw+jz3RATO5oSSTYhvHutXb2Pmq2XEhfaXG8Z EHUfnS9kJoGiiqMYEMcRh/s7HH64RxgHzHnFtEj7uvDZuTS0lFFdoeuYGQCQuBQ1XkUyLxGYbcXW 08PUKKVj7C6dAFCY0Ek0tp9WGtHMjeJ/rWP6vq1qFO/IrWCg3nsBMdprrx988552jVy+R8a0Al1g yx9rye/ZBP7RI/773//tH9eYfyfOvyaduHFsgcHvklxc3L+pHxoXt3eCC5OEyIZry9yoJUWXQGAL 7NvimwGq5cnvNqJdW2YFJ6UL6yvFNdcIgO7Vc14BShjvIu5/uENBEtEkQSKB6nquiYfaSm/Ht5cU XNz3Brh4a1Ka5GSrdrHcHs6cFFlZ/AWJrhkRWNQNhCA79UDgEJERMJeI8xrx7Tnjt28Lvj0VnOcB KY8o2KEgSB4KVUOwWm4RgriscsT5vIBZfsc4Sp4JQHWUASkD87Tg5eWE02nCMiekzAIoSMISSZpz iwxqLq0SxyJbzo2OsSvhK4ykxLgiSylXbCGMrEL6IGggMGXIEjNAJDB11gYtX4GC42S9RiLmriJj OIAQSVcFGZI7h3wSS04OSSBlEguK6n1C1TjRfgPAfn/AMAxq38EewhmkAEEZdjflQqP+tHlyITGt z1W3xyvz0yEP6lzm3kvL7iRUhuemsMbruV2rzVMO6uBlt8CIc5FQ7CweX/MswbcsZfgQoucysTb4 eKhEjS42At9x2Ph3oKL2d29DUkGFgREDF97XUBAYZOyH/R6HTx9w+OEeGCLWnLCsK4qF9Eadq5J4 jP0tpipkrm6lBjKyAws0QALqeWJggDo1xxY4C1DeAM0W2HSyDDSBpfpxuBbYrQUzXWdfPX953Lrn vZvj22Vx/5Xbcdd7X81oeSk9eePtvgbaqrZ2hAAh/se/vu4t0p+/3ejK5PtF8Ra4uHbPtbe09wQX jX4HuOjAw+1329H29Qa/vtE/pg+DE65aICuRs3N6n98ik0DWhlrdi9MXSlmRy4owAB9//CBJ1ciI usUtACJtCaLW5BqIuIRxsAq9eyFcvWf7HqteNTyUOslfk5pUyUXQ0M5a7zgglYjnqeDppeDz1xXf HjKej4RlGZDLgKzJvSiIMDdQUaNM9fKgEcQDcgZenkUSscwrAMJuPCAOg6pPVg/znVbVx1BQtzuT LpjNhEbZVOkDVBIQo9k2NLsdBkwVZJEFUxK7CEl9Hev8QdGdneb8CBEhikFmDEFinyhiqURRJTVN kC3AvAOEq1dTrhZc9O9WiFrHR9uWckJKkqEymPpK8+sII1GmrM+EGLE/HLDb7wEC5mVBSglRk4UZ 9zZjP8Ciixqxvpx3xnAdEFyZ6e3DdcO2Ib6bWenBo+25Sh83z1jditcZqGNszEeYsQI2hmQEXVZM 04zz6Sy9SyTp3jXlO+nOx9rZtvut47pk2CRQqKCB6nUDFXJL/QdAQ03L98DqcaIdwYHAQ0A87HD4 +AEfPn3E7nAAAKxLwrzMAhJIQIH6tDWMnzUI16VBZmnuNVWf5/5QaYeAAfXkaIanNSg1yU17XgCX jJv1Czf10tb1QKORVNwCFttTbnOxPXdz8GxMekD0Fqu/UKXYDgY9r2tVMheSO+5a98YbL2qweXYL wulGEC0jTReT9raevS2XmvuvHd8DLjoJiiPuSwOYvg5119OBkoZ5XgNA/aKutdnW7jVwUcWnuBxg BRiBqBJAiI0EFBSYiJSL6oZZYlkwClASQmDc3e8w7AOGcRDCyKxs1FRFNoabfYtX/G1wcet4L7iQ Nuu9Jrb33BIqqdCohkJQo+6WAsAFpPYVTAM47HBeAr5+W/D1W8HzC2FZdii8B9EeAKHkFaUsCMiI KBgCYQi6OyyEkiXa5TQvkidkSThPC9ZcEIcRDMI0r5jOC9a1aGrtoImbJHZBVit2KLCo0TeNyUq/ xaiun2gZLzvPDiSZPS1CJtAwVqX8TApmtB9jiIhRjE9LTgDDLevByhA0s2tmgxcq0WiBWlPPOp4b TwUlmiKpsIinjctqEQAsdbZ5o+NNVRwdhwH7wwHjuEPmUuN/QKKCWmJimyvG9Oqh/XaFzlwYTzfn jTW25A9K9HsQ5ZP12ixuCjDCQ9WwEGZILTd59AVqaRbpowI007pinics84K0JhAIQ5T09kEDcMHc mq9usW4fW0kzUEFDG0m/rvF6qgUa3my9LsaqVaojeWfIg7mNhz1++PgRd/f3QCDMKWFKC5ackQjg KPNOEpuZfQZ7cDZzxS4mmfOR6W0ujNG7LZGRFv9no1rpTlGjxzbbgD3rwMEiajarokoa67ywtteO 6hHoJYB4nVm7GvIav2zm8LWPvZWYb/724zLU66a+3wsq3nPcBBfSkdeY9i3VSAc0mnNXr7/CmNuq XTt/EefwyoKqVy/LqDYQ1955vV32XN/G15hvrUEbzaKdRB7hk6votq598ZbgUiQehnAYZRMJoAwE xuHDHvvDAZk1D0kQs0RzQ92CC2/lVQlG/7vuCX7vsd36qZi7yWHhKc4RlVIEJ/TMGUSsCbpGFLrH y5nw7VvG6RRQcI/CBzBLojAqGUQJA2UMoWBABjKjpIR1XrEsqzJy7ZUgqoJUMpaUsa4F53nFuhYA UTPZavjnEDAMgwRHUvsNBvkOS3aE/S7JUoqHzXoRxiRqB995s3qV5Cw9RqiRQrnuhA04csmVebLE ExCiGbt6WP+zs4i3x0ukEeKuCoZmIJXkb207PNSyRvYMIYhqicjVKuLRK5KR3W6H3W4PoiAp3lP2 OdZgnirBa4is1cuOra5Y1lWzHbVyVAAAIABJREFU60az066Io97c/G5F39cAxmaf1xjJcr3uDMt3 Pv5cAUw4KVXRsVyXFcs8S86OLNZBMUYMIda2NlX7HvUIkVTAaaXVzwrtuBkcRDjTJWufWiz5+LC7 sBYwioItkLit7g577O7vsP9wDw4BS0mY1gVzSlWtoSDb/tYQ4TXmRKMUdGBu/1oDTtg1G8sGZNiz 3LT8Yrz8epXzueQYqFKR5n21HJ+oF8zmmpSim4ubewjUXXvPYXPp1u/tcbVOW5D9B47L+fkKuLjG fK8XcuUlOrlfs7m45s5yC1y017bwYQt2XKR6pZpEtquX56+qVfhKm1GNzip4aibjjfoSVebS1p9I wYWhU6osQEqWXWQpGUOI4FJAJSsgySjISGXF/Q932N/vheSxAJEgnAW2g2pbcaNnm3Y0C+VV/dx7 jvZ5mWikhMh2dLCgVWoTUEXxAFEBxYDMA5juwLjH0wvjeGQw3YHCXmwISgbnhAEFuxiwi4QAxvxy wnoW4l2yhgw3ujoENVAEGISUGcuccJ4ySokY4g5DJBBphE1VW4zjiDjsNOw11SBCJhLnulRLaWIk KIM1plhK8bwbhjJLKUhJGE0p6p6sBpQAacQfjQOQktpJ2PNBwU7w4FrsszR43eqwNGvJeW3xNUmk ahKP6skd3WK9v+QiIIFV/RQiAkxqAlA0+xpJ/73f77Hb77Hf7ZDWFSUnyavS0AIHTRfz6brUwq9e 0I5NhZu2Xh69K2H7CHuDt+dVKE9UmTHJfAKrq2VjlEkKIlFqQrJSMtZ5xnQ+Y14WMBcMIWKIEQh0 QcO+B2C08S1MjQADDk7b+99ovwOwNPIGdj1RmgLYQEAMdc6F3YDxwx3uPv2Aux/uMewGrDmLwadK YYmC2BRp/IUqkfAe7QFDs6Zc6m9J6XResjJ43ox8tYuxsbFy7J0F7TjLGOpaNj7SzLMtz6rqBmpK eeWg15g8rszP18q7dm0zh99Bwo1d39RCvPl8v7mvzyq4+H/+9f/6x/aNRL3+sy1MKn65o7Ait+ev V/a6r2wg06cqQ/JCVVrg4OBag2wxXZeOuOOhu2miu88WY1vnOm82xKwnP369A2VXRFR23SUXqBsJ I+S2uWBkDBRUNl4kdTRnFE7IJeGHj3c43O01mqPUxdSNPWHqWtbV53ICWsX+OJpt30lqsEiyDYao RSI8WZhv+axns6Qvpx1Ad0jlgIenhNMRGHb3AIBxIIyxINKKSBkDMTgVrJqlMc2S88J31b6vYbVV EUAlIv8BhQNyjiiZpbygxFMBxDCM2I177HajJjcrnhm038EasbPf7CG4zQCSgsSySKV0Y1CKZEFl rVeMkmTN3fpQd3wUzNskOKAxSQopgnUvElPLAF4xD0UtPeI7REA9FgIhRCk7N/XUV8hYsnrAFJNC iIuvTfBKtOX+IQ7YH/bar0WMAEt1DRWmTJ4o0GfrFQmD1KOqH9q2yW1qmNowl0vmUBnDrRVuxfp9 1Nzf8YVa3xpQrfY7NXUnkiVWCmNZFkzTGeuyggBRfw2DGri2NFBphDWXau2tbgRyqU3gel+7b2iK a/q3pxnmntoa4XpCLlgyN8jGR58vAHIk0H7E4cMdPvzwA3Z3BxQA87q4wSe7vQ11/d6qKkwlZ+Cu Agu9lw1YVF7E3ceAgl1jNyQ26UjXc1wBhXXYa2C270nq+vc6t7ucYX0xtAEfJuHkG5/rRVwf5e8/ 3qf6vn5PCzDEWwR1obpxkVePLtQ2rcEIYcMsO2bWBOuALVkj7PV8UJF4YACwMMxKrIIwolohqoPR MAsjYkGZmfysCzSSLJj23dIZUoawOHUDhAKspq09ShMu7uCgAytF7+3BBaHt1wacGBFqgIXdU/NS MqodhexNR9Vn393dI3PWRKIS9b9aekvv+siwxbGoo9eLmKVfLdWzCvedmdS2bkgxVe8BOLAK4GD2 CAGsqdARBjAHgAa5x6mXvpcCuDAo7lDKgBAPWJaIh28z1nkA0Q4xAPs94+efIn78FMFlwvH5CfNL QlkkOJQzD4LuwgrMWXOgpnUUEKJ4NjACSs4IaQUyIwQJG84MDcIFHA47xAhwSSh5hS0O8c+HJBwD wFGYbG4CWdUQ3MGDDdVBFzWIpZJmloXV9q1Y8Wvvmy2E7TiY/QNl1jK0RUdR+rduuKgSXf1rRnUs OdfrnO94uxcggImBlAvWnMAq9YgxKiAqPt/MIC/GiHG3w7AfQSCkdcW6rtoNkuukXcNiU6LSQzU0 ZltbDWHqSF1Lgoz+X90I8cUjBHShk93zagPQtvjbdvrg6o9jUkUZ/rrGpQ1+O7hImvJ1WnE+nQFA konFwYObgbwm/pFgcEIriDUoHFtbzeOhykWp/WsosZbgDJaUuTogIwBsFEVLccmr/HaVhexMEQ87 3P/4CR8+fcKw2yGXgkWNeg3w2Zwvvl4VCEDULg44uAlk5qSTFXCjk07UXCE2wtVduvvL7Xdyga98 KjDveWClq/V3M3/q9OjmVjtVbm3MO/SEXpJwXbLQzVidmu259xzXy3rteI9XifGN+B9//5//sEli VsyektwG1BrWd6+wBmOyTbXYJ3gFF21zpAxuyt/QA3dhbCZcs0OsiwJ+rhJC2jDHChLairQAigB0 gXX04mWcip6StYRXqmwAx9eO8bemjA1ho7bd9uriSx5UQEEIlTN2JUr7/Q4/fPxBMhBGoASJ2BlQ VQ7XJo654mLTPlg/8eZ+q2Nd2WgPGy/2EoLEaCAJSAUNTMWajZQQwQp02EEFPA8Dg4AwoJQAhD1O J8bT44JSDiAOABLuDoy//XXETx8jUFbJ67AMWGb1ldfcCcYsAoAYCAN5PlQBFqjzRka8ACkjrwUF hDCMQAgakVAkSPv9AM4JzFlcCiki5aLumoRMrKLtUI0gTVICgkgbGk8O1N0u6a5e7BqE0FIgl3zI YCgUJikxBkJEECUIMzLX6Ik2Ps5QFNB2Dnk65OX/Z+7tfiQ5kjyxn7l7RGZWVWZWVTfZQ3JmOCTn a3UQsHvaW+hFggBpdw/CPQi4NwGSHgQ9SLqH+xP6L72DtNrdm+GQ7GZ1VWZEuLvpwczcPSKzqovN md2LRnZWxqe7h7vZz77Lq3dl/p4HGPqeyUIOWeup1KgR7wjeCfcTlbIDeQFH3nuEVY++7+HIISap S8JZ/DeIHViZmUWmyHMtxJfr92xrGLj1CwzzOTkhjI22pu0gt5zGmKg9je09NuOra7KAEP09b5dl UdDxZy7j6NiBExDHiONhwPF4QEoJnqQ+iZmobC45reArxcQqaLbwXFA7t6gA97KeSw8aAEItM20Z YKXYM2Y6Y3QogimgUSLOwXUd1psL7PbX2O/36PsOU5xwHMRUAlA1g+j92IqeqemPwbo4UOrV2FPN TwMw+UT9Jbh6UjCVN1QAR24Ysb2DOjWWDLaZTM14tuecm4rvZ8GYjV97kTR5SbeXXLb+PgUWi/s+ 3YpHrn1ODx67o/zzf/6L376e+TSYhG8nnkVNXM4/JxHw7NicxbepbYX+14UsmgOG1JUQxgpkgDUl EonjIgw1ktSAMGnQygGLHbcywjbZVsWimiURUkNCmJY5GeI9/a99raIgChf2RcNBpZ32G829y32o ftviLi54ZAlpsr4XjSUnhu88+nWP1cUKiSISkkQqKJeYo1wUZoHSFxt7e76KQsUwOxcXuBAdNxsT bkCMgAZJ0y0aJw+QaCskq6XY500iYaia1TVj7jWMkzwSd/j+XcS7+wxgJVEKiNhuA3Zbj94Dfeix 3ewQcIH7+wFjnmCeBzJmQvjkOQI+nROmDFeZJCuBZgAxZ4ycEYkEKHkHgBHjhPWqRxcCgg/isKd9 AgPTlEx0EkdcIpjlitkc4RwsLbbsz8azhJk60ShN04QYJVFaqVNRABiKWtpBGDk5Aqxwmo3Aghby 4vdcNJD36xYEqj57biqdM2uSQlNxQkriMxOCJhbjXExDGVnSSpNWV93UMOBxmiBaCldSShsjBfLC lGGNqPO1zVlwetJpmwuFaKTG1v8K7Tg1a3fBeprBtf9IF3WlbUXp0dqX0DB50/0z4zgccTweMY4j ckwaKeS1XomGALOEcRY6yyjAxWb9sv8z9kG1H/Zt2q0lz3vONhsXBcnCSTK6vsd+v8duv8P6YoPE jOM0YJhGJJa5wpD8L9bamfMn6viXrLcQMJyBYrKp8K3xhULznt/LMGsvPiRvRRv2+dxt6Wowa8p7 nven3X4cuAAD/s+/EHBRpbf5jc/5XdQkSedHsRL2xoFvJkWjTm4qU0U+pAmeFSDY4hHJdonbqIAK Kvev02mmnSBVtcIYb5XsncqyUl+i1S5UcDEfvNr+2SE9QLMFoVJNARjLeTP3ARFerkCC9Lh9DGQQ Cegghu8c9rdbMYnkJExQHQpr2xpwZI1vm63PKaSyBReoWMPs2HM/iblDphBLqwGi2gp2yuLFx8I7 30wF1X6Z5geMoOp2ph5T8vjuLuLhmEF+BSBj3TvsrjpcrQM6R+jQIYQV0uQxTAljHIVRAzOGVIho myGx+UNwlZgGpCgaIcIiMqjMfQDYrDdYrdbIiZASELoe3nfgJKAixyTvgZRZQk0O9g7UoVQHtqh+ jRFD296WnRb/EV/mTdt6MoLuSMJSG61Bkc91ES0JNsHUvDwbqfY8AfJ1LVjbiKiAHtnHyJyQOcm5 TqRvhibhgukt5bneexm74BFzxjAMZZ4yamZHp/TG+mMCRjOBT4BFdQw8hRyzHAbl+JLQyznL9S+a VK4AwYBiUe3XSJ/lM+WPRsgp9LHe3RGQYsTxcMRwPAIaOeaDR9/1IANeCkRnfbcaMM1W13ezT/9r n94Cjx+zFRBDKPKJ8yIIXe12WF9utBhaxGEaEXMsM880bq1pSvAjFwdUAxQJjExqQoG9lwYXngHC HwIannPOc8DBc7cW/D/Vnj8t2Pgx9zbNxRe/ed2iWABYesYuUfsSF9s5BgLkN5Qg1wdao6vCrgEa SjzkXwMWyH5XW9l8sduyqg4w7YJttQenQEm+JWcMN4uwifu3waLlOFjfmj7qd6u5WI5hed7i3tYe x/PxJeKG8CuwExFDFiUx9jc7+E6zM2Y1iyyIVtufOifnUlXbpuZln2ysGh45rynyRU4BRAeiDkBA 0WI4rz4MUrdDVP2+uX8uhDw4Qs4A3BoPo8O3dxFDcnCuAyPjYuWwvexwtQqgzJiOEx7uB9w/TMXH gZxQoVxqVsg7Y1UrW+0MNO9SwIUkzGLfAU4Y3hglcsQqXeYoGqJVv0HoV0hMOI4RU2b03Up8GxIj p1SIo5SORjVtKPM30EsELVmdChBwTjQcKSctZa7zR7UtlsRK3qn2xRGgEQeyZozg6neLuFHXYJnT xhjLvGkmAM3DUls6UQCIE7+JmJI4ejrTVOna1bEvjBwShtmvV3C+03wYk2hlFCy12gTzvbC11zS+ aXMLmKiM85lZfmZrmVCzNvU+zGcCtZWJ2vFWg0FlLOcCmc0L0nMFXNZziIGcNG39MGE8HpGmBEcC yLzzIFABn0QOCK4qUtv1a8t50eHFKfOA5Q9kjgShYZwzOAkgdt4jkWTg5M5jfXWBy+st1peXgCOM cULMSQCaLQ+dmQIsNFU4VdOdaS0aY5OO8gI4P/L9x9zOMvkf9Zj361j+9FqMp+//lE+IURT/51/8 +nU9ud70LLiwietOb75sWPEvKNoHLh9H7XFC9Q8QRuTUTs8KVYtj55Lbmdd/4+wo7a3LpAIBAlEu 1xUCRQwrVC3HT7U2rXnBFql96rnNIuX5NcvxLA6wNgb2d9PDogcgc4qz53F5tkQrJFxcbbC5XMF7 LxESS7QDe69PT5iZRHtmcdi7EgZpLZT3Rpqum1wHRgBB3mF5p66DCx28C5IV0nwylBll6y/ELyKz Q6INvj8wvrtPiCyJrBgZV2uP66s11t7j+O6A79/c4+7uAcMwITtC1/cwx9Cc2nyVqvBX6Z3zXCo2 n4/M4jMCSH2NnM1ZUoimd1KwDK5Ddg7vjhO++f4ODymhW22w6np4kmulpkiSDJ/y8NkEsgyZok2p FSLbEPpaAC0jTpOoyFU97tRHw6IrpPYHdL6YZEsi+TGQNYHV8v22OVlqmXY1GbXqaW1vYaiYazGq iQlISX1HGFr4LIB8s4aV8Trv4UOH0HXoVqsSmjvFqbTD8mp4V1taILhrXKQJ6nNVgVOL/t9noqwb P3r0XO6D85u1gBcAp2qq7H7Ff8MJMCUAnjyIxRfj8HDEMCuEFuA70QSyjkEpJHeu5TTv//LQ7Jqz N3j+5ong1LzCRMiqZUiOxNG5C+g2K2z3O1zf3GC1WWMcJzwcD0g5abSQpiVTdXVx8CTTLlZ6Vub7 hzf5ye29Wopz2wc25jmmlWc5VT4Cos6lgXjkKR90/+YM1VygZXS26Np/NjdtkejlvES/dWITVTOG c2biyBUQFBaq0i4CiAPATlTDRXEKQfSNFFEcHun8Rw4miOnA/Dgqtq0ASv6u5Kp9xnwQW+bfyEiY vwT527UzvRlTWJub82e/tf2lNJSCCRtH0jYToKp2AUO+I1xtL9GvVpJkiSoYOZ0DFdhVYtMSXJ6B M6CRttWUJJ78+q0+FQYOxVnTC3M2X5gSdqrmJ9IqpCUc0lS5kkTIMYPRYcIG394lvD0QsuuQGXDE 2G56bPoAHka8/fYt3t0dMY4JCcLEQhBHQcuMyWxvSlXXxtvKlBCgIeMr/XAlgkVSqjsmcJYEUC4E JCY8TBO+vbvH13fvcDdNGMkhQSpgOu8A75Eh/huJLYzQn0SJlBmiL6t6vHMhBsLkM6aYSvvJFllZ s/JfNqBBVM0JBWIJcODlxGDTKaIs3kZhIEy+zGGbL/LAmsaZS59sDsUURetCEtoKCDixEtxADal1 5NF1PdZ9j8wZx+NRogtszTJXZ2QmdZKtc7RY84Bi2jNJqo7veaI5Y7Dc0orZEJ2BHAa8WHWLS4pQ EIQ8p9ygZSIGLlir9UoEnYBZM8Fk5ClhGiXMOqeE4AO6rhPNkLVN3/lJp2xy1OGbgYhTPLG8+Hmf wi+UBknCLQK8A7zUChLwKtlvLy4vsN1tsbnYgIlwGI6S8A58qu1r6ajN1cKfTnvwwZu2vY2k0lEr Q3OS9fRk349tw+muH6qteAoAPA9gnHvHsp/o3DH96LEZuLBvp+rHuXbilFmdxiIY47MJtmSMGVVb AZgjpoEMYUYMHxL6FaHvPcTEnMAsWeq5aBlQAId5lFe/Ap6110BElWSqtqNtN86ACxuTCiyAJYmZ aTDATbVTbQ/X+7TLwq6lxd8FpAmVRNVcyHMLeNF+xjzhaneF9XotTl5+qVhrVL0EDWHUOy0X7Znf ZRE3jrMFWLD6V3D7Dj3IB6k46jycC+UYg+BI0m1DC3+RU/MJa9IoBhg9Bl7jD99PeDd4ZNcjseSz WHkCTyMO37/Dw/0BzBKZMXEWIsaiUXEq4TNjEYFhxI8AZqSYkFJNJOXIg7JoKhwgPrWphojGzLg/ jnhz/4DvHg64jwmT7zCSwyEmTFMEyMN1PbIjTIm1gJOE3VkBKwtHTTmrZEbiPArM6nYUYi2vQYiv vUtj2kRFk+A034QjAyh1gjmvJdWJhQE3hNNCXW3BtgyrMKWFk11r87fjZn6ydZOZMcUITrlkH6Vm PpW5CQEaoQvo+h7OO0wpIcZJ+uWcRkhUaRWQ1NHtTC/AsEFHxva5WbtPElhuwNgMVnCJ3qjtnhP+ pdmxjBDbwVaRr2PPud6DAavIaqDX3mVOCdMw4vDwgGEYAJAkeHOhYbLtG2l+lffa0jKg/WFrvQW+ P+RDFmNbnkclAVd9Z/ruFb13fY+r/Q7b/R6bywskyHyJnLUIIFVcuFiX1fQ1f5ePaRuerXWg8tb0 u6WhCw1Qc/4fbVvc7rnA4tFQ1+XtnzxOzffyI/ufBBfKn/xffFGjRcrtbGE2qgmRqBfajCYhVbuZ g1cxR4Cq6rYhKEU0VEdO8hmrdcZPPrnAL778GD///CfY32ywWgGhB4gSmKW+hurOCoGtjM8kYQs4 NKLrdFBtYFXLYBoVjVTx/ozK2IBLI81V+xI194UJfbPjM+bQAJml5sL+llabhG2+CMu8G3VOp5yw WvXYbrcFOFhRs/JeqaqXnW8dYNuN9MlN9EdZuHWfc2LuYHYFVIjpSsI2zfQh798DCAoARUMFBIA9 MjlwFk0HI4A5gChI0jC/wdujw+/fjngYHTh0ABIcEgJlpOMR8TAiTRkMj8TAyFIeHKwl1FVL49TE McWsjL8ZcKCopSVaBMUO7sBiP2agJGUiQsyMwzDifhxxzBnR9cg+YITHkBnDOGLMCWNmREhxs5S5 ZOBko7IMCV2FlKEXU4src60WHYMStFrnI0Wp00FEJfU26/xw0GRKjFrjRIulMaCFyqTfqUSqNATa 8km0IEKBWEnv3cwrMdO4Yh5h82VRcyYAZAVnOWneDedlnhjTLKxRJnboOmw2G/gQMI6TAIxiyuIS 5WMmkZZlFy3nDNQ3M7xddE9shVTOiJyCJ5k4uqtda+ZwqpqWorHgs8dNK1FAGRichSIZnil+V5YU LWdM04jj4Yjj4YicMzof0IcgOYNQ6aLdxsKEyfmaz6ISq3puy0M+ZKPmDzJnb1Y/G9aQ6UoBCQI+ XOexutxg/+IGV7sdKHgMccJxHJGyRJEUYMGat6gxJznVghY+Y+ZAXT/FhEbNfh0bw30zPPbIGLxP G2Bza6bFKJfwo/vLXz9i/J9rLnnKZPLUde+7frn5v1CHznITtpwPQBGF9KZzTUVdvZWxzm2aOSv/ J190BKKuTsIsPQM0AW7EagO8eHmJT3+6xW/+7BV+8eXH+PSnL3D74gI3t5d48XKL6+tLbLdrrDc9 VqsAcgzRali1u6zOaJb50ZVn2uSzBgtRdKpZEbNJ8U7WhWDhnETiyLYc7LnPBAoIMwK5fDHyAiUH wMw0gSZiBKKGN55S/EhcC1rsvqyMIMN5j6vLS/R9L/ku2gnbOKcxs0q6wDLHsJg7SJ0NhTiUjnHz 9kmBRQk5DXAU4HwH54IkUYLRTwJnBX4cAHRg9sjZY5oY45gxjhnDyJgmxjRpqXRa4e0D8Ie3Ix4m YcBAhOOIi87D5ywht1lSeE85IREjax9yFudWm39QM0lMYlZg5qKub30LTIokcMmSaC/YmH1ilhLw TBhSxpiARB7ZBTAcIjKGlHAYI8YY1Zyj87FkzRSCl1nAXAYhu5q4yCTIMo7NvLW2mjMfSENtvdRF sXThMm99PT+pJGjr2aJy7N0TzSTqltiYpofL35XxuqUkz3NaIOtP/k4plbwF1Y9K6QSr/sQ5ZM4F YADAMEgCJsscCjZpkmZMpWhNa8tP1qCsLZ6dtdyWBLTyAaELxTRO85POkueWaBNQNCEFFbGJGuUk OwRYyP68HQ4arjyOOD4ckaYIx06L2xHUhQesQM58M+r7a0AF1Xk2B0ofuJW1UvtsBnAD7yX/GVH1 L/IOvu/E4VPDVhnAYRDzmCern1SFABP8sprZLOTZ2mGjXUev8c+p2GT2iuo7+uABOLvr7B0f2/+e 7fm+E//0m7XL/8UXv3ltzM2IRQEQ1Gor6pAVJ80z/wSUs0xoeHAmEMRJs0r5DFCCCxH9JuPmtsfP Pn+BX/7mE3z161d49ckWm0tC6DPWG4/d/gK3L7a4fbnHR6+u8eKjPT76+Bq3t1vs9htcXHYIPYNc BChLuJ7m72fNkUFU8xwAqmIFS0y+pqN2Yk8oY2BVHwko6uXWBt6GxhZTDBbRIM242sB7NzczFZBB NoZCBoS/z59ni4k5l2vFwSwjhICLqwtwkKqNdk9guXgqiZpPCJJ9S0DhbL9TMGGmETF5ONdphVMD dbpwszyD4EHskRMhR8LxOOFwmHD3bsD9/YCH+xEPhxEPDxPuHwbElDAmwtt74M27iIdRfQ3yiJ4y dhcr9M6hcwFgQsrAlJOGYNbIBDGXaCAvaR8cqTkiIydjctV8ZSGTbFKGJsOy3BQMwpQyQuhB3mOM GeOQkLOUdifyiMSIRAXM5GhE3c8iJzITImtFSIJqL7jY5QvDZy59MdBu2TlzFpMLARqJ46tOqjWd NPcax6k4R3a9vDuLSBG/iDpPGBVk2Jpo53ad5DJPvAENbhh+yZMhZ8cpNsy5ajjF58PABYPIl6Jx IQTAUdH+SBE4FMm0Mt+5yp/Kf8ZFUNap/GzZOJ29R7sVdsW8xOYVrlAjoCxZB8/DfReHylNmNUCg vjezbtTfcYo4Hg6YjqNohkgAmJRzN2dr07bUjlTAhNpeo0WP9P85G1v3qHyV6I/GCF7awYA4fqq2 DiFgfbnB7uYau/0OIXTIUevvaORWeVPtGtEBOwGGTzDhpUQ+FwTn4Pr5Uvvz4cKHRn385wosgAZc /MsvfvOamklAMCZWF5nQ2CptmwqvXlM/9pepou1vp5yYXAZRROgydvsOP/3ZLX79m8/w1a8+wU8+ vcJ2FxD6DLJ8Fy6DXIbzGd3K4+Kyw9V2jf31BW5f7vDRqz0++niPFx9d4vbFJa5vruAcoesd4CKA CazVRK3mHhlyYgJnwFGAd71GqagmoQFcXJi9SWjmoFr5MMDNsflkNOJp20koKioKLZoUlZw19ERp t31LhEAh2KQVLB2w3V/Brb3gggoHYQ6tzpnZx5Z62w4FEAYprb/GmJsMm0RBgUUQnwnVZgioENW3 V+DhVEtxeJhwfz/gzZt3uHt3xN3dEfcPAw7HiOOYMI6M4xAxHEccR8bdfca7Q8QwJaQ4wiNis/a4 7AI6B6kiqdJ+UtWrqJMtjLlmwpT4f6fSOkn0Rq5+Fq1aWNT0rMweog0xwieID8UUB4ccGWmMYt1z DhSCZBjUMWXmkoFSTBuiyUul3Ra7r4DC1pfzhcHPZkypVU767sVxEiApfmX5LoC5j4lGeeWUxTEw xuIUCGhUDEn4bjGVCLUvBwZPAAAgAElEQVR4Us7PbeEzR6UmSs4ajjozryj4TFmAl+b18D4U3xGJ LpE5mFKCDwHb7RX61QrjNOHhcCgSthFn750k2MtVM6ODVNakcb3COpsOEdUdRvuqhDt/AzP/ijPj UTOLVA1FtYpwc2Fl5vY7nwEtFVFwaZ9VpjX6kmLCNEw4Ho6YphEABJT5UGkYo6HLpi1p2lS0GY+/ 62dtNB8Xx7YSFrxCSDAAUr8ZARhwBHYOXSem3uvrG1xcXIAZOByPUugNoqkrxc50XVrGz6d4bwso Pqh7T1z3nGiPP9X2pwYcz/HnaI/5/0rBhSE+pUGzk8vLsHEziejMCJoNuC5qhnMMxgiiEV2fcbXr 8ckne3z1q0/x1a8+w2c/vcV238OFiMwjSpImomYRasQHJTiX4UNC1wPrDeFy22G/X+Pm5QVefrzF 9e0FXnx0ieubNXb7DpeXHqFngCKg5bxNGpQMneIHwNkmnSX0aqNUmoHlObBoaPDJwlyOkWkggLn2 ws4VIKfIXPNZVL87A3XCgIJqX1zQo5ywubjAercRHKDXFOdAtgRcCzjYaC0kqfuiUimoaCUkFNWB SPwjnH5s1jCrp7s6aKYEjIeIu7sD3ry5x5s373D/MOI4JMQpI0YgZUlElbJoIWJiRPYYJocYxQbN KWIVgN3lCmtPcJa11ZEk5TJmylCQQ/LR9lt+Uzhh7pbngjkVrVQLCkEWAqrSewkhFRV/0hwWwQX5 HRPSOCHFhG7dF6Jt2S4tdXMWuxgyEbLTEDuFvQYguRH9pJS3/g1d4ORKxInXSIuYkmQ7JB1/zSMi XvdWpl00C96JzjwOE4ZxgANhs16j6zrEaZppFZY0oJSi1v2tb0YlPq6MoUjdNRmWgA/J0BqTONIK DqwZSJOoZiDeUKLJCCHAB49u1SOmhBijmEkAzdrLEsJJZtE/J6IvuJ6tTpqDkWryVQ2FScVlSdXg ZgEtDXMuzzb1mWGaBpAwn1xiYGROO8oVKECEym2lpVQ1RZZbZdDsnpxEC+adq3VK2lsRNTS9uF/8 eL7Y3KAFFHawjFJzoI30Ke6uCkb7VY/99R5Xuy1812FKCVMWh8/Mkqa/4MVGAC6j9x7twA9hyk8B k9lz/hnAxT/l9tiYzcDFX37529dloikKXpYKB5TxlYGdM82ZlE7Vzuscw/kEpgHkR+z2HT759Bq/ /u1P8cVXH+Nnn7/E9c0FQseqXcgCTNjCFmVySWZCIYgWNQJkkEuQ8twRzif4wOhWwPXNBtc3a7x4 eVk1Gy+3uL3Z4vp2i82mR9cFzRUAiNQmvhvkAOcsPYuqzEFNwqVqEjJgUcelugkt/U9mg944hso9 63f52H9uqVYFCMIgnEqH3gdhhimBgsPF9QVcX532vJW/LuDCnT5RbACozrCn4MKcN7mED3sFGWoO 0UB0TwEpZhwfJrz7/oC77+/x/dt7vLs74uF+UCDhAPZgKxhGUjZcmLkDU0BmOebJweWITeewv1qh dwziiJwjPAgUNIeC2pZNWJZkmQIiBXSYz4BXIq15JZIQKZDljqDGdAGkQvkctDxtAQ4Ock3Q5FrD 8CAZQgF0TiM2wCVh2MRZvOCJdTipzhlA7m32cVYGYr4fylRSSvpObF5RyRcypShmA4JkayU3N2so 8111HYicMKJhABHQ+04dmudpvm0OlyloQKHZfwJAnNr6GzNUUeWTOi7a+U0WUlgFWW2xAY6YEhiM zeUFVquVmIOGagZAzlJs7jGqXgSVBV0rIKgF+fI+zFJwErZbfhdk0ByDrlGu+83BYI5QygUFWChQ IVCLRdCw2wKg7Z3L+MmzyvinhDiOGDQ3BjE08dbS0X4BJpbJyX7EZtTDfpSIERtvIzmLawjKP0Dq MyWoJ/Q91peX2N1e42q/hes7DNOEwzCI9qvhX8AfB1C875wnjz9jCP/0ibA+fDvXt+eYhhbg4s9e txNsjjIbokHKWBuGuvw4aogdMeAifJdweeXw6pMtfv3bz/Dbf/FzfPLTa+xveqzWThk5QKpyz1lT RWtWR6uYZwSNleBUBM8FyaO0g+E8oV8FbC5W2F5dYL+/wu2LHV5+dI2PXl3j5Udb3Ly4xP56je3O Y7VO8N0IHyKIWRmUaShc6bM4dmqIbbFvqFaljFk7gMvxXJpFmrFX0FYcPAnFLFLuQ8LQgvOS7hvi /Z9zkogASuivevSbHiH4ouoGtD8K/OZGLXM0tAwbpbTX4hygRoZororiqlWjdSgRDocBb757h7dv 7nB3d8RwiIgTwNkhJnVkVD+CbGGqSnxSBmD+OuwQPOA4YeUZV5senc8gTiBOEF2vRCt41yP4TswZ DAUNLGBFWVs2RAgy/beMW1I/AFKlj9opDJDYLFUEIDVcXNUG9Z2EkMY04e2bN+DM6H2AD66EX2YH JCcRGknq15b5oEoqLUktJpPCgPQk0zqllKs6uThHCiBMKSHmpLkEZI46fb5NJWRGICchjCAcD0e8 u7tDTqlEaOBM9k+jBy1YsX2VTpCGDzq44BTg2CoRUGYl2p2ZbzgjpaTFrFCyOhKhhNyyCgHOOaz6 Hqt+BU8OKUakaQIxwZOEqjIt2tust3Nk0WhHPafmF7FrqMybx/mG5UWon9OnnoasNiDE/lDwWlJg o/pwZLS0UOafaIRqxk6Ck8y1w4jhKABjGqW8fQhB3os+r+11oWFFc/Zhm5GtGfbSJVcSYbl6bvlw HeuiTdSaNuKL4dGtV9heX2N3c4PVZiPAM8ZSzp3qE09A5DnGeNbnom0UKhjC8oMz2pclA/0n3v6Y ppFzPP4pcLHc7//VV3/2uh6dfS0uMD+EBaBwMilN3S8WjQT4Cf2a8eonW/zqt5/h17/9GX7681vs b1YIfYL3mreCWRYDOoCDpn029GkLyBai2mzJvKmN+VldkMroAJM+lWB5IHQOq7XH1dUK++sNbl+I +eTjV1d4+eoC1y9W2O43WPVrhL6DJd/S/HLyu/S30qNZjLrNzXacFtKSayd9+yILy9cKqBrQXfJm 6H0cAO+DZOPUXA4pJZAjJET4tcfl1QVWfQ/ONekSszj9nUpwdcWLA6SNpb16BRfaOuertqLWa7E7 EZA9Dvcj3r65w/39EeOYwQiQUuNemT1JBk1mmB2eqGoYyHkpMAbAEwMcEZCwCoQuoFQ6lZkJAJKE KYQOPgTRXGQxFQhokUgSpuo0WLzoNQcG62RzVqxAX3IrYDC4SIDmmVGceZ2Uyp5GSdc8HgcE77Ba 9SDvEJGA4JEdIUGeiSzSdtD3CoakOW7XYQFDGtpJUso9xVTMOaaqJ4ipJWVGyjJfvfMSukcoUqxF VvR9BzDjeDxiGo5IMWO9WcP5UPop2p+lL4M8i8u4QGcuqn69bb+u6Vz4J6NdRGYySipMBN/BOScR PQqs2JLeOYf1eoX1ag1mYBoGxCmW+WcAvTKC1mdEzR76flu4MK/XJ70rghQWfTq3Ncdr5BGXVslj zezS3oub9QNVmdmosr7bxjRDLWCzjzJjbqIySMDnOI44PhzUVwHou04Sl2mb5pqMx7ijjSA/crwZ hqqUKd+Mx5nvHFhoe9r1ByNP4ptBIWBzeYn9zQ3219foQodhGHEcjkpPTEBbzMFmvp36tZzvcUPa znT0kb+fsT0n7POfe3tMe/G+Y/a3/8sv/sVr56hRU1VnzerUKRNcQqKcVpV0Si8MdGQQZcCNWF8l vHx1gc+//Bhf/upT/PwXr3D70RW6npB4FIattuTWLg4LoSIjZqeaEtnqgm1Z8vJTCJ/uImKwEzmU PKPrgH7tsblY4Wq3wv5mi5sXe9y8uMbtyx2ubzfYXq+xuezQ9QznxBTDlMFanEnaWKVTp8lhBHS1 sTZooFCjnmQDHxCQRWKKcdreAqHIKns2EiWg6ZSFuAjBkF5fbFa4vNwIMCIt1ewDmG351ndXB8kL sJiZTQBQzbbJs6RYut/aQiyJs2glDpyHiMMQkTKBNEyzmpcAWM4CNqlW22Yx6yzeCKQ6B84JcRjh mBBA6H0PR76oT9kYvDJGQItoqTSbIY6PrAzHUft2FOwkzSDo1InT2IGI/OAYlVdSYfTMwlRZnRmD D1J46u4ecTii8wGhC4AnJAI4eDEDJQZHBmUHDwfSAm+SUKuhtvaybZw1mkKAE0q8P1lhOfUpEcCk 6b4tnlHXUWFXRAjBI3iPNI14d/c98pQQnEe3WoGCrwm+SEwYDhLaTY5KNlFh1A7m7G2NJaUZDhoS 7qq6ewbIGyAXJ8lpQd6VfCliwlL6QzJHQugEHJHUpxijOrUSVYaswKa16VsOEFdAhLTfQiLbFTKT ZAuLZ5jUTwBq+YG6Fb46I8J6RWG+1DAZ+a7+NliAGSoYZEYGCwCRdOLkWm6o85ZFixenhOEwIsUE cJ2rRCTRZWRL3wA1K1mmQhKKs3nbrDJGDQBb8p5mGOv4SprwkyNUgVgLotD8do7QrTpcXF3icneF /nKD6IFjmpBiBBIXLaDVImGikk6c0bS1mVcF5BkTNfpsgK3Cn0I35nOlrrF2W4KJ52oDzp1v93tK g/Dc+z71vGW7W+2krfmzHx0P/6++/C9ey/utUrJXj3VuSvo6EmcwI8jCPLMwL54QAuPiqsPNR2v8 4pcf4YtfvsIXX32KFx/tsVo7wJmUh6YB1iA0C7QpNPVh49KOUHN/lHVthDUrwbBaCi549KuAq+0a 232PmxcXuH15hRcvd7j9aIebFztc327RrzxWm4AQCC7YfbOCjSw+FXU5lG0+dtI01zaTrGi8FTEy h1Iqx0FQgKCEBALU2qygaYpYdR6XVxuEziE7LXzlO+QEGBwo6dhBYHZgLTY2oxwAoDVALK8FQ5wi TfKfv6YA5A7DyDgMEcPESFlJkuU90WQ4xhSLBsPmg3Mz4gZSRsMsHvH3B3h4BNeBTL+qlp2YooTf QfKYkBcHwXGaJMeFuYYIVVSmKN85iZNnRAYHUk2QAmun78YyecLARQ3dNY/1EDw6F5CPIw53d5iO R2lP54HOS40F5+BJtEicGJwAYokOSZTLHC2SbhVuJSTVtYSDlWkq01OfDamLYrVVtL2aQ4Kh/ig5 I3iPVd/DAUjTiPu37xAn0YT5voPvpYibJF+sYZGFpQriATXaLqMbdSmql47zMwfQebROJWgpp9kx I/7WL4DEwbPv0a16UAiIOWEYj0DKsGApU0K1BMXVaSX9IJOOjUkULKGja5CiARbldlzmaWG6DXiY c1kq77Ld5g6xcwI9kxSZaiE0LqNf2oOyv9FosD3XARmYpoTxOGI4jogxwXmH0IUSms+WEbjQndYX pLWW8bxrxaRqLLftdf1uWbNr9rQfKaFe7788w4AjALjgsbq6wNXtHpubHdaXF+DMiIdBtLnewwVf NGNmYpHoGYJp/Gp/6zypYusCTMzAm7apubb09wnmPwvpfs/5H3reh54P4Gz7mhvOpnA7ZrJDKJL/ q69++9qWT0mrCl14BAUUFqLYqIoogREBFxFWGdcv1vj8Fx/jl7/+DD//4hVuP9rh4nINiUrL5aa1 DcuFV+7+J99mT2nAhzFrQoLzCaEnrNcBF1dr7PaXuL6+wouX1/jo1Q1evLjGfn+Jq+0Kq7WH8wmg ScAWAFZPZlObS8K4jJqzVs0slAGnGUrJnN6q9C6AYznJG21HaX5FsilNCN5hc7XG5moDpmxVSABy SngbP5EmQ+bcec2Wk9e2iGkDEKdNkVRF++LUoZMz4eEh4f7hiMPDUZNESW6InIBGBGqAHsoCT4DW mqj6KDjV2kAy8919f4dpGBGnCOclV4NTVb94jzschglTTvBBKrFGTfHtjLlxocPFd8bCQaeckEmA tZgPqoOj+BxotVUW4iYaIZk54uMghaWIgelwxPH+AdMwgMih73tIITcHcp0QOvNA1fvmkiZ0hohL nosiHTlqwlKr75EVeRLQiGrSQJPwShkGIGvce4/gJapgGCXc83g4wpPTSJJQgJ+kZbasi6qJm1Gb MnXmi073e6/mllzri7aSUqmsGiNylrw13jfJwHIqUUJEhK4LWK9XABGOwxFxHKDiaLHdmxbW5Bth YkCJBqrTvfal7VNhQqeg6aTf7c2KRqLetFHYFMB0Xk2+3GfrlWd/U33oDAAV/jxL+yjjfhyOeHg4 YIojnAO6TrQY7XlmKpnTy+YUG0muQFcA22lfzsGIR7dHQMXpdfpM7xD6Xk0l19is12BmDHHCYRo1 ikqEZsoMl7W4WqGjjV9VyewptydwM3xUlmMZ1gZMPdWnc5qEZRbc5fnt9lznz+eEiz7Vxratjx1f gouz5wDwf/XL37w2FCnxyCh1CbzZeI0QO4LUnkpgjOhWjP3NCp/+7EY1FZ/g1Wc3WG0Cuk4dsrKk W3bmWHY6Xf/Jt6VKaSYdEAAkYfZgZcIZzknZihAIF1drXF6uJPz19gq3L7a4ub3Ezc0lbm936Poe 680KvnMgBQ6ghKw5N5y3sFrVHLiEkgYdgEdd5K2Tk7ybynRNsjATChUAR2BK6FYdtjfbomJnaNhe UzRa1IMOQACjA0y1rQunSo2Nw6aqxS1PCLFoMXJ2mMaEN2/ucf8wYJyiRkl4ca7UsuUiTQNg1vTX QhQzS/ZL70LhRSZ1GsF3JN7wD/f3uL9/B/IOq/Ua3qvzLxHujhO+fzjg3eGInIHQ9ei6FQBCnCJM fW8aIC6x9fI9xUn8aKzglkYutbKi9YE0IgIwwm7j7EvoX44R43FEnqS+Rq+F1eDMB0TNFJmRUixp 5kXWp4qCCozUsWgyY1piKQk9taydKhBYJk8FGl0IReNUJWkFst6DgscYE473D4jHAR4k0SWdJLLK Nlasms5sBEIlkmU228Vyt9L1LaA41WJou1sC7AwcWb8SwAI+QpA52PUBKUXEJGW8CUAgAT+UIU6f aIrDGXbTOVYyRxbeXdQXMMfKsypnm7B27qzPp/SuBRiPM47WZ6PsAUxDUtBDlatB9j6t3ZUbltbq mFrY6nA4gjkhWCn3hSBZ7ktVa9Sy0sqmuSnM9iO2Bbg4OaySP2EeaeS9R9evcLm9wsV+C7fuMeWE 4zAgRYkscwx4RuF1Nu/ZOWRHJTS8bcfcV6SaBFqftfpXc+ET28ws2PTrudv7zB4/xm/jMfBc1uUz 2+b/8qvfvHZOHcoIyjg82rkqjowM8hJaGkLG/nqDz35+iy9/+Sm++tUn+OnPXmB33cN3DPPxU5ZR mCBgiV8+uN9/1O3k5RBAqFoW+yanCj8nY5F5gnMZoSOsNx6XV2vsrje4vb3U7KE34rNxs8HlrsfF ZUC/cgg9wXsCVEORNY8mjIE7TU4Fi05pjpFF41iCqwo0HFnUgaouvUdME5xnXO2vNFufQ1KpsJhs it+LaC6ADpJIq1KI4htitm6dH0SSpIHZiQ8GewzDhHfvjri7u8cwJmHepKGlrDkotAR3VspevCoI quVgiP6M0a5YbohWCB4xRhzuDzgcj+oAKCaGh3HCN+8e8BAThjFhnCKYCZ0y9BS1NkbW3BPCIQuw EOk4awSJ1XXRfjDER0h2CoNiaSepyZDBsPLpAND5Dt45TMcB08MR6TgW36WwWsGFACZISnH1yLP3 U+ampQxvpCzYE6j6S1kisZTNbGPt5mICSTnDSsvbcblZ7Ue/3sA5j3Qccby/x3Q4AgT40IE6D9f5 YpsnNZ+a2txs/jMpv+USjenGaU2S1tTiLDtn03/JyqmAybtiWmHOsNLzAMMHj4uLC/jOTCQDOGUJ ZVbgINWElbkSikmEIOaSmR9G2wc0fXhk0+WKs1ih3XmS2pOb61sYsbgRGRCpDp4F0JqPib6Lotzg aqqpEIQLicspYRiOODzUDJ/eBzEnaATKnNu7Iqi0uMNhMTw/hsYvpszjpzUaE2O2nuD7HuvdJXa3 N9jfXKPrAuI0IY6jFM+D0VLV9BkobtZWYaQNsCh+iS3LUBo8c+rB/PizutwKuHi/puJR7dkj+36I b8e5fT8MrCi4+Jdf/dlrIKPU32CxUUu8P0AugzmC3ITQMy4uPT7+yR5f/vJT/Pq3n+PTn91iu+8Q +gw4SSssgoywDYBRNbdGWP550cU59dTihPl+64+aOYQmZw0mnACX4APgO6DrCZuLDpe7ldZE2eHF yx3211e42l3gansBFwj9qoMLfqZiMwdxAhdTvlik5uYkZ+BCm+opl2ugACCmCJDkBVhvVmJ3zFag DTAfl2w1QhRcQBN0gZoJbw6oRABr7QKNzJG07gExAu/uDnj7/T2OQ4Q4gHoBEiTOn1IdUxhFVgKY Ve1oibsYUN8MjZdQk0iR0JxWLXWEOCa8u3snxZuYEWPG/WHAQwISSb2OlBkxJsRUQ9s4owCD4uwF +QaJiYBzRtQsksxmnjKPGLPdVg2GM9U9RDvEut/s2XEYMR0G8BSRBnU87Dv4LohKrGQwsrT1lsSr lRQaSodGA1ecT3VTfGzMZh46yiX6w5i7aV4EkHjNiRAQIP47h4cHHA8HZOaSyMqYfNGsMOvca9p7 It1X0FTkX6KqIbU1ZyCYzMciVxMKV9+lSjChwMPDeY9+vQI7SYs9DZJMynH1NSq1lXW9WBiwV3BR HBQXm4G59228vHj+Es/e49G7tgeUplYNWQMsCq3i+jrs8ion6TnqVKst5SwZW4fDgOPxIPPKe8ne 6tX8CcCcugvYhz2jmm1RH/HB2yPDf3peOb+enZkRiZG9w2qzxm6/x263w3q9BhgSFVVq1FSt4/K+ M6BHzVhrA9u+tnoLKg17fw/eBxDaY8vPD7n3Dzn2x9jk/gT/X/+Xf/WaM0OchSyHAcF5ArkIxgAf JlzuPF59ssfPPn+JX/7mM3zy2Q32N2v4npExgrWAmNzcJG9T/+vqUvpZx/2fF2Sc3wyiajKhkgMC DbFs1WYm8ZFeqnH8HgjBoe8DVusel1cbbPdXuL3d4fbFDW5e7LHdX+LycoXVpkNYOTifAcei7kWW qBQLgwWUsQrzp9qkSjXKpNY03I7hO4ft9hJdH7SYGRfJQFw6xSQiH683NO1NM7FRlZ+uJNSSXBcp A8Mx4v7+iIfDKIW6LMU1q6+GRTDEpGmfVXqEagO0PaLRZa1+qWPuGuKmwMYpSJjGAcNhwDAMmIaI ISbkfoXUEMTMQIwZMbMmxar1PECmPamSotnyJa120lwhGgmkjm+2WR0S0nY6kDqsEthBtEbeI3NG miLSFMExgWNEjGIQCqFD13VgrzkilDuUjJ4F47bSDTUcxICJFm1SOzw3OWJQmDAULMt9nGbyFCmO Clhw5ND3Hbxqie4f7jENI6C5EoJ38CFIHgsVWwkKtqhqBHSF6LuwprrynkG1qqrsUimSef5bCXzS 0GqrVyLCTC7auZwzXPDwndTVyClL5VwdxyxV5AAFh4TGwdOWD9V21/FuScQzCDzNbjXHFmX1LXUk raCzRCQVRJw8mZsnUPW9aZ9bb6vP1snAmoAr6Nwdh6E4exLEjEQmeJL5sDQag5ah6lo6STr2Q7dn XF7Hicq8KsoaHXhLvrhar7G73uFytwV1AWOKGFNEZEZiTS8gQ6fuAS2Mr+aTov1phZ3S3DoeKIfr en0MHDzl3/AUkHjKtP9jwMdztCCPXVefLef7/+G/+ZvXzObJL6YRHwDmES5EXF15vPp0j8+//Bhf /OoT/OznL3D94gL9isEUYfkf6uRSIlyAhKHqVvoG6oz8z3GTDKFia29UCGUWVz+EdhoCpjK3CIKK d8kRQufR9wGXV2tc7dbY7i9xfbvF7Ysd9jeX2O032O0vJIZ/s0LoHNhJ8TXRAFG5b53BdTFRGVP1 geCElCN2+y26PgAQwCf5sARAMTswRPsg4UIOoAxuNAa2yMBmnpGCYcwOORPGKYsDZUywcFNmh5QB JjWbOC0mx4xxEp8G8ROQNhfnOmY49YmwGhNA9XcgknuYg984jBiHCWlKYDWFJO+RSJ1OtSJvhuR+ GHMCa00CqxdiavJW+rNkY0lNCcJQhCk6V2PviczJUBxAg/fwWbQXmcTcQcHDdQE5Z8RxBEVGmqKo oWMSbUEfAG1XUA1IytVp0taWAQfZZbbxCsS88yimBzbmXAkx67vMLP0ikCYEq8ydlIH54MXPAkCM E+I4YTocweMkfdV8CeQdnJf1YLVMToGFEuJGGmyBa6s9LD45en7xi9BxhjIES6aX7b6kpetJANt6 tUbX95rrwaKFqmmxXb3WJjRM6uwmk7C0rd1akwYtjp/cb8EcTx/SjiDpJQYUa3Mb1LDQZ8w7Zbor sg7WRit2U/VyFo3PcBwxHgdM4whioLP8Mc113D6DTAar0RcfvD3jUnvfs3NtTsEcwPU9OUK/WeFi t8X29hobBRnHacQ4TQJEAFBmWGrAou05aRjN/ixrcTGm79PcPMc88RhQWIaHPveez9nOPfOpfefb qGaRf/O3//Z1F1bqaMfIeQTRAVdbj1c/2eHzL17hy19+ip9+/hK3Ly7QrwlECXAJcFzyO6iVt5Hs T5fo0ob8vln02CC+zyP2KUeZ5TXzMDA77st59VY1WZSBCmr6UUmVK4y+YIxC/jMyqSbCM0LnsVoH bC57XOzW2O8vJNfG7TX2N1vsb6+wvljh4nKF9apH13VStEkXUJGgDACQtpOdOEVyAnPEar3CZtMj dF5CzRzEiZI9WM0azJpl0zkJqXWoOTVMymVXgBXgERPjcBhwHKKkyCZh1uI7SEhZBoHJg0nMKOQc xilKZkU1vwGk0Qf6jlDHvjB+AkxzkTNr5VtIwabjqCWnCVPMGFIGw0moqnPImociERCdVB9NJmHZ e85ZmKpJ9c6rCcXCIiGACNCwvWYOobaTIAXVHJHqnLKE0YYa4ns8HIEYwYmRxgk5ynhT8OhXvQIE IKFqAZKg1vk7gRHT9rc57aJof4qUqu00BskGPhqAwSzSauaskTsOvutE6h8mpIcB03EoWTFD10nC OU+A5vlgFSNbJwuzHpEAACAASURBVM2ytgq4oCoVFvxkDptybeKm/c26FTOJdNh7Xyu5UgUcICnc tdls4LuAaZpwHEeYs7IoU7kyhRIxVJn5cmvNMEbl2mOPb3wWQ3CrcSjqqQXtJJodK89s1ka9H5f3 K4Bu1gL9lr8kkk3Me2D9rb46xJDCckOUZHCDVCp2TrK6VlPCPN+BaQ/sWDtm1r75OD7CnAiPH3tk q6Y/Ndiaz7qun+wI1Hn0lxtsb6+xu7mWOjWNw6cVNbA1ZXO35rNRrZEB5WUjWrrcjEEr/ZVZz9Za 1H2Fv8z7/lwtw3MjStrrHx1frv0x+tH+j6KhOPeRzf/rv/6fXm/WK/R9ADmGDwP2e8IvfvECv/zV T/HZ5x/h+sWF5qqIyIh1cZnUQcpUy5CeMvd5J07cf541EM/d/yGqnXq8NX204KbtRzslCPPsoATx a6hEQmK25WOgw9zXJclVgvOA7wjdKmBzsUa/6bC/ucLl9gK7/Ra7/Q7r9Qbf390hxVQBjkb21Hfr yoQwqZrB2FyucXG10RwKwmClxobmttCKphr+IepyFRkJEsZl4ac5E8Yx4XAYMAxRNBRlwqmmgSWN d2JCRk3C5UOHGCOmKRbnQiaoDdgWY/0y0wATJLuo+QfYYmdgGiaMx6HEzcfIWk5dfAgkfFJtsfY3 KuMqjCbLG2T117BFlDNrFcpKXKx9QiMqA7VqtZIQjoo/BEjMJhQ8Ht69Q0oRgR0oa+rrJCHMq74X 4AQqNClD+9oCCy5QtkhrJjH7kuGWazsbMFLNJBY1AzWPtBo30SqRl6JhwTlQZORxAk8ReZTy1yCC DwHOfEdIxtzpva3AmwEEbsavrC3UtoHEJCsmEJ6dX+a0jjOrc7hVj7VzxGynwMY7hL6DCwGJsxT0 ilLFxmsILSvzN/8bY1CP04i6cbP/PI0xje2pr5nttwlfwJ4ysbn02wg/doAX92q0b3Vu6v7mZOuv PFOoQXsvx2LwJAXvw1HMjlLHRZPUmSnLgIa9W0M2s7bRCbB4OgmUtPuHMMuqBUOTIE02KxCYCZLo ruuwubzE7nqv1VZZqwRPYqo0s+LyGfY/VQ4w6zuVwzD+cK6d7Rn1zpWR2wq07dw4/DG0FI/eywSs wgvLmVh08snN//V//69fOxex3gDX1x1e/WSDX3yxx89+foMXH2+x2jiAIpgiJHrOI5d00ObgY0tR ZtXJcx9LzH6yKcG3wT+DXp8bvvO4yvL0vDlKRGH8NTZN+mVpYmTytX1t750hoayNGaj1OwE12KqN SjdVOyNTRkZCWAWE4LFer3BxcYEcGd/84TtMozhMuqIaUWaiYpijAM6sOR0ypjhivVlhu7sAvEns Ql7NodM5L4yVM7yBqywhW4EkWVUcIo6HCceHAcNxQhwTsmaVTI2JQxw8GeOUJL8FlEEqQ5tSxDRN xUnPiovNJIYCVvS+Bpx0IZvEBZbqnuPxWKR3ZEKKETlq1kHvJd02uJgqLE9DZmjEA4omislr9AZp GXCx7Vvti9kMpJZIyLGYJXlPFxqHR9J7OYc4TZimsdRCoAzEUfN2APAhSAnxoAXWRJyR7uY8S5Us 9UJ0FhVmpbOySd9tY12KXWmfbZJaMTQfvMQweSr+Gw4kRc2clNqN4yRRP1NCHCcJMQ5BtDPew5MU crM098wNSNDh4PKTShsKgNeonZJ/pHHoLMm0OINTLu8Aeh+TvImcZGTlDHIeq80aIXTivHgcZH3o 0sz2z54NnP2cIfENe6iA053ZX4lBY9Y6uZ8et0vqkM0AmIHF8sDmj2JCeqSdJu4U9kUNCGmYsyW5 4wxwTsgpYxojjscjcuKSVj6U/CNNsraWUTbAAoAC56c3k+xp8e/8W5l/Cks38NTgsLJmnDgRd12H 3W6H/fUNVuu1RBjFEUnDbWWs5V+pdzMb8/rOF6lECh0DngYC57U4p2Gqy2uW17a/P2R7il+evedz wMX/+Dd//Xq9mnB97fGTn1zgs88u8fHHK1xcOpDPyJgAypqjQrhihgPYpFFDMi2bXG5L34TTzcwG ApvOD9IPRXA/VIMhkpO1o0TCo0gNLC2dZzzg85+G6VQ1mF5RiKERiZY5MdhlJJYEUWzZCKnDm+++ xz/83T+Cc+NkBcXYRpBY+pFTFoc7EGIc4QPh4mqFbt2BScqmZ3bI2RwzVZ2oxMCxREcEeCASDndH fPuHt/jm99/i7bd34Ezo/BrkAnK2VN3CCJAdppgwxSx+DokxpYyUoVVIpd5BjFLpsrxXezf2/knG zRhhSSxmxYxYTDd5mnB8OCCnWFS6FiI5pYgMhuuE+Um4pt1Tn23AgrW2ifOILLVGnJoLskaPVH8A mStVa4AiIR5iBDknhNdwJaDhrpIR8ThI7REoWEAURh1jlPEPDqvVCi5YGG8dp0JuuTSjEEGbZhZt 0Ur6NluJqGHWctw0J+ZDAUeSQVXt0EJcA0LX67uLyGMSB904iR+Nd1hdbCQ9OCoBt/FLOaEMnQEB Wsp3VEytpqmp4KLuK+sVXDKRAiJVF0rlfc3G6iWx2Wa1kuJu44RpHAGIic1yhIj5ryzM9xLrtv0G oIwBtYCjnlHB3+mm9PORZ1YcQZibSPjkPHkKFwBeQ1jRzBRetA8AqPjcFNqi0SUxRozDiOPhKM69 kIrLXs2ARJovqWF6bT6T525U0U75WBdt/j4JMvSrhI4uzyAqY9P3Pba7LXY319hsr9Ct10KzpklM g8SNRs/AXovI2lFvdj1TCD4/APWapz7L+z8GOp792HLNM/jme25NRPD/87/929cfvezx8mWH7Y6x Xk3wboRoKzR/gi72zIyYxNkNzfSzqBBaAosC504dH+cMNWO+1UF63/bjTCDnjlk/WPPKz6Y4zskF c2BBIA6SlrqEeTr9SN9USNDzTUMi2TuZWEAFVPNgaSOzwx9+9y1+949fg+BB7OAgtUSs2qXT9sNs hSSl2UFA5ohu5bDdb8HktOpmB5CYRHLKIJYwW8cEzwEuO8Rjwt137/DN777Dt79/g7dv3uHhbkSa GJ46iL9OELNBYvVT8AI4xJEAKYsvRLTKl8wYJ4mpL2pgZTY8Iywoavsi+ULAhfOidvfOgWPEw/0D 4jTBAcg5oXMBDHHkm3JC6HqErlPqYFEpaIiEmnJgWhZzJK0SXlITRiHrBi7KdJIj0aQdlvwbJq0J GCL0q5Womg8HTNMkBNkRckzIk5iMGEDXSRQJnEOBNBYtBAjzL5yMamZOm55kTLqOpeWLKOBNCWWd weKAavPJoYIuOAmfhXOYpohxHJWxM4ZB/g4+YNX1ZXWbL0eRyArBr6DMGCEb4C5NXxonqPxPqP4n OVXwIVEkc0IrvFiys27WawQvYdnjOGhhNFfmRKspOLs9QjtOiP3sDm27T8UvWnwvn9OaOmZ0swUY ZWfT+hm/s6tVY2W+HbzsK5fszEWTVLi1XBdHeffTqHkxFEgLsNP3bMBlAdKeMocsx+Ok/w3TPbe1 pKMdhkK/y3IR2u7Vx4iJ0G1WuNxtsb25Rtf3SCljTBFDjCX4tx3vNrlh22ozs7aamx+ymcPucg6/ T/Pe/n7q+GPnzY49cp/2hOcAH//v/vd/8/pmB2zWEZ0fQDQBmi7V3krrsV5yjiuHrEyyEdHK1jg5 ngUXjzHr57+UH/LynvOyS7cZaBR8dYGdIT3GYOSIB1g/IvOhRJyUCai9P9eUss9pUS4Hzx3SlPGP /9/v8Pab7yVkOGsxKKCAuppRVMIpJVGjhGNNcYDrGPubPSgE9SnoAUgxM2KGI0bwDi57TIeI++/e 4duv3+C7332Hu2/e4fgwIY8EZI/hEDEME5hJKliqvR0s0SSZBViQD2DymFLSEuhA8JLUxjJgmrRG GkJrAyQAQMGFCnxSgE1UlFDNBeWEw7v74tkuJE5TgXPWqAuAnIZPFo2bQ4HEJEXFqhOhzHMr/mW1 F8R80KjjCfo02RiSI8BAlA8eIWjqbDafCCeJoaIkekopwTvSaqya9jpGAAwXPELXw4UAWAIoqxdi tnSCZs6UsVsq+FoNhkWIGMM3ObpUW01RQjsdoTNQJF6sklHUEUK3QgYwxYiYovi3RAZiRjyOQidC QNd3ZZWHEOBDQG7WetUc2Zjbfw0RJwMMqjXNWfVslVkV+sHQsFQBNEarjAl4JdhdJ/lFGIxhmjBN UzleNEk4pS0njPzMOa3GopxjFxf54zRZEy1+L58nTs2VhpzV4qLJH6LEpfgToYkeKuNVZy3a/bCC dFx2U6GGmuIuJUyjRpRMEzhlOFAxkxR8s/C1eI5UfSIkLyTqR6+l8+8ImIcbWxE9IoJlK2Kto7O6 3GC72+Fqt4Pve/HPSqnU47GxrKav+g1qucQpw382r6LTMWgDD95nsnhs37PNKKT9euRd1XX6RBeI 4P/9//G3r/swwGGAo6TZIS2ZEjU3g83U+o3qrDRvWX2Nc2a8ZMw1fGresCqxnHibn+nEY9tj0SaP nSM7SGslPAIsSluaxFLw9hA5p4iPyuy172TjVQZUtBpUAqDkw7aPPTwCAnU4vpvwd//x7/FwJ0W7 iL2YLkrzs2Z2qBVZOclznQNiGuE6Rn/RY3WxUUYVkNSJMXgCQcI5D2+P+O7rN/j677/Gt394g+Fu RJoIlDwoezh04EQ4HkY8HI4AgNVaEnXFKcO5DuOUME5J80qQ1OvQFA593yPFCM4JpqKUzKHCPHNh kDWsTbQawkyJxIkQ6h/iABzvHzAcjyCWwNqcUgUkAGJMFWD4oK/AwIUk8MrQqAym6mjKKM+EEtvM ltBJnQZ5DjAdOSBLpEhWxg/tJ+UMytJuTx7DOGIYRxg5IgKQJTXzpBE11AV1SvQo0R7GlNkiaSTL qAHWVuaXqpY11NQKOJF6AhNR9W9IGXGa4DXyQyR/uZ6d5kVwDq6X3CXjFBHHCS5ldBFIxxH3wwDu PdbrTSnvXkNHFeho2y2Et+yHLjPUP6wCLYHKWNe1K+PuFLyllArACF1Q8JlL4jlmgDyh63uEvsOU E0bN5EnMQKrFGpe0Y0m9ltuT0l7zu/SR6v6ztM2eV0TyM888+YvnrmIAqJSubcCEXcX124CgaQpL LppiOoR+EygLyJ2mCcPhIEnWUpQ6NV0o2sWnJO7H9z8OHs5dZ8PzGLAg+6+5NVtFYQXNGSa4ePSb DS73O1y/uMXm6hKZGcMwIEXRjPriOm6USR5A+k6X4GLZv6cAFilAAT0CIJ8BUP44USNPmGKeCy7+ 7//tv33tEOFUVQT24CxMrmZGtJCyolcqs3fuU9tGgdDiN7CcNJZ0axEkXF7MOXDwvgW8vO4Hnwco szezhimHzXG18TM5AzpkhTbOoAooLJmYkEPzlVBAQR5UklhppVGNF/XoEDjg/u0R/+9/+DtMxwgP idqwLAfCRc20xMJoWO9P0NBShguC1K+2W4TQg9wKEnUq/hvH+3t89/V3+P3ffYO339zh4fsD4pRB yQNZAA3gwZlA5MEMTFPEpFEDkqWyR4bDw3HAu8OAMUkKMBFupf9dCEhxQk5SG4JZwIV3odrI0cwk c+KjmpOi6zsAGd5LaufDwwHD8QhoxVVGrhk/La04C8hY9ytVhVe9D5NoLkrOBN0sTXDW650n1V4w kLPmG6tmBgakDST5FiYNX/UhoHMESpJHYhWkUNRxOOJwPBafFcHwwgWnGMWXgcSUEvpQnCQBqFOq OK1KUjAFl4XQFe5V7OAAEGMshNr8WIpyEgBS0nLVEt1Cmjqeta8gMY847zFOE6bjAJ8ZqwQgZhzy hAjRfoTgSyE6QBzCnfeiaTGmtWifgeN2zTtXpU1W/wqz59eABQVIbZ/REGwI6M8Q00+neTpyyuKY Ok0zoDijC4sdhd9TM0/ryY9eV47PwIX9/fh1BqTs2QZ4jQKxcVcjzXYfk9hPSF11LDWNWqsJ47ZT dg/S4BCu+6DavGmKSObHQpCCfs6XsOi2X08BKWrf2fLcx/jZE8Civff8PlTCvKXrdQEwAfAO64sL bHc7XF5dIfiAKU4YhxE5JX2FdX7O2l7210eaya9BH7PxmPX3zLFzv89tPxRYzIZldt/zwKKlKe+7 l/8//5f/7rXyozLo9VphWkV+NGIEG5/qRzD/fmxrVW91UdHi37n7/BAE7M68sBkUKI5x9dM6JJU2 Ug0HbJtUmd7ykOlhaH4ucXMMgEXYmIMihCXUCzRtOouJgtjjzR/e4O//n3+Q0tyoKm3rCVPVgJj0 UVL1ImnCKGAcIy42W2wvbxG4RxoZcYh4+833+Pofv8Y3/+lbvPv2iDQwkCWFN2BpvMUfAeSKRzU7 h2GMOB5HgAldv8EEj+8PI94dB4wZyM4jk9c8o0DvPeI0IccJyAk2AiqHlvda3glRcfKD8F0BMurU 6p3H4f6A4UFS+xohNtDAZJItIcUJSIy+79GFTtSdek6Ehrw6eQgxl6iHZO/NOzGVZAYiF/OZgBMH 9m5GpDOLD4h3Dl3o1L/CSd0G8ogpYdQ4+/rioO9XUpcb0+tDwGq1AhOrU66ZcWQWWHZBWwPtXAVQ PN6lcqwUODMm5UgL4EE0LylLRlNommQBIJrXBqxmB9UkZEaOUpQtk6iYU5wwDUfklBBCqI6dJJoP eFfqfGihGVXlPyEYEMpMKeYhs1GT0g4CWB08cza65Wb5eFCSoQWs12t0XY+UEoZxQsxZzs0CIH3J 0ovqVEhU6uHYey60QDUEbBrLpaN7Obes3NPvQnsbTcNZWjP/FFBhDBMo2r5TE2yDGgq9M9hS/zcz QIU0XAA+lzsJd88xYzgONW8ESUSJMx+9wlytfdzwAJ6Jotz8VedBQ1kbXmUcBM2cX35mPadGS7bY Z+1iFQR9CLjabnH94gabywtkZhynETFnWMp91oy8Xh2ZAZsjOmfIFa1d6ZFp56mFI03XGqeRwq24 Pafhlc25df8pb3wKmMw1E3o/ms8wa2QBSk/cmwD4/+t//ZvX8ssWBDddsm6d68hjr+79m6mYazPm n3Nainbf+8wdj3l4tK092W/STSEYi4XXtr9gj6Yds1MXLdDzLATUpMUZEWiewxC7b6AgQdqTw+/+ 4ff4+j99rdoUKOOEEEwyx1HfTFolD9qfDCAnRpwYwa9xs3uBNDDefPMG3/7+DX7/D7/H99/dY3pI cFG1FCxEOIEKsMhKamJKJfQ0pYxxnJAjI8JhYI+HMeIYpQ5sIo9sJoic0QWPOI0S0qiSN5tNkxq7 sQHDwvVQxo0BrXsgBOzw8IDDwxHjMBZgbCGsZUZLJzAOIzw5dH0PFzxSZiRisEa3OiKJkmCFfGzg AcrQIarmJLkxQKKZSer3INikSpY5ST4P7z0oiDmBmzk3jRJKm0toJem0EYDDU0SaJjgSTUDX9UCo FRyFnymBK1yEC8Nr0KYyeNKQXKnz4iBaSQfrj1NzlmosFZQ4r+3SNeghgMc7h3GacJwmRBZtlOOM PEUMg4R99n0H33fIxMjEpbaDI4Xzpot/nymTqt9XyqloMQpjaCqplhTyhaNXzaP5Ejjv0fcr+K7H lDPuj1Ih1Fn20pLvRU2NhDruRRPJzfSsDsCnzW/NyJXOkr2/FlDQ6TUWcmyU5SQsv5yvEUJq0jgx PnOBFqUt/z9zb7rkSJKcCX6qZu4A4siIrGIPV2RlH2aXO/MUS+5vcl6in3NXhtMHq6u6qvKKA4C7 m5nOD1U1MwcQEVld7CY9BYmAw+Fup+qnt7e3JuKCwLzJqwlLIK3YnwEW8c5aDGzJgvk44Xg8aiXg lFVrNw7QbLkt1T6Rp8kQoCRIKZb9dh0asAYU0p3Qk5U/VXDRxug17vQSb9AuafsEBSEGXN/c4N39 Hd69v8f2+gpZCvaHA9KSW60b++dCjWf59fIGPbN2TeeqIfWtbzV1E3eBV57BkjXP/CVaj6Y5Oc/x 8cIjVr+t97DxD//9H//bb70ktzPU8w5e7ti/1/FLnFS+Bly81bLX7n/xZmdd7sFHW5I1prqCD9vu BNT04ad+KNItEkeMIiAwWCKoRCzHjH/7/Z80BNTv4YvAJFzd3+uGVtQpLkELpBCWpJEeTw/P+PGH n/HTTx+xfzpCipqCKOu7sZ2aUMormXpoqLdF768hpsdlwVwKZgGEAzIUnCiwUsISmJGWGdlKm/uY ifWnNr6NdofbyBKAqbc3QdNlH56fsX/ea+bIbvrUZuwbVL8pAKZ5NvvqlfpkQECROx8KWEZSaxu3 AmcEUgfYIuDi4M0ACik4CURwzZ6PDxEhxghiBVkEy4ZZMo4HDU11bY2ajHSsSBTMpZSRSsb26kod MtmLDSoj9RSnUvex7Rkx2u+MoU8jLwKIJQ6ykjJa1MujjHT0gxdm60wTJArwYogWfTFjXqz+iK3F lBKWtOg9PPqljiPXDJtrZrc266xCcDuNlquQRZp+VSxPhfdVijSTE7Uoo2pCsSqgm+1Os8fOM/K8 qG9M3aMtas7vBa+U7IxMzBzlBUt8X3Z04PVD2vurDKCt49X5ehv//SUa1z/H/up49Toqpdt1J5zG NUR6zzWjc8qTU8Jhf8RkVYuDabr6ku714dQ9gzpwsTLDnzS29npN8+pfv5Y9eZs6RhtDwPXNLd5/ 8x67q6sasurmSS3C2PyDAFr5OqnGlWslYO9vbWv/d9+Ubr1fAgiXzCpk5+ESZu2UPfoVJcGLwOLk ma99ZhDCv/zjf/2tnGgSSNYP+2sebwKFNwbz0vE1LX9rcF7+IU7n4uz7lgZdD+nrkMglvYr7YLQb M0VQDmCJeH484nf/4w84PC1wB9Am96zD0cQ3hDH/6pRlIYMlC47TgqenPT5//oL984R50cqfIqE6 gQItLLNm/BCYv4XWJEFpBBSkab/nRYuHaUbNARTcaRKqYjcJOS2a08FrUZRGppsUC10f2VTflbDb /dzRL4aI/fOzZr5ckpaed0HYmY4hBJcastfWCIxxtwN7hIeKb6oOFw9VEzV3kPqsMHN1wXVpUyVy k3SgGU1ddSykURoozqQZUpm2Vk2djhOOh0Ojn5ZgywvALTmr82FKINKsmZtxW4t1+bhkqeXpGrnv 1qryHt8/ekKlfHV6VLOJEkUvwleKlzb3FOGuszKfB0umVAowT5P5b+kECDQCZZ41aVhg83UgWoXM kld2JVTgBbS2cgXd3Xl2s5/T0EbMyeY+i+UmccBoe4NAVUpmaMg2ccAwRCCnuj5h1zkQczMMcAIu gLrmahnorq1t/F8m3Wf5Xl45ui1y4d4dKHrtWS99L2ua0u9FP5rEfHIHkSrJS9ZChfO04Hg4ICcz kcVoGkHANVE+/1mc73XSMwHN368/1nR0RZb/QvbV34OgTtdEVqnYsu9uthtc39zg/u4eN7e3yBAc 5glLycjWdg+Jd3pDJlyt8ANV+G/AogmoDqRf4nOvaiDgv6Gz8wC+Ej288fWrygCd/zW4oPbFf8Tx 1Qz+rfv86me/Pvq9s5le3i1yIgiZA5u9qOb2dGm2AYoV4KjaGYAQUTIBEvHl4xP++LvvkBYoUydn a03dhhXIILgg61JzET+n2TT3+yPmOQESIFbADKJShfFBNQPUexCKqIrPkxVIsQRHxrAEZGg+Wero AI4DhBnJwieZCFKyhjCmVBlj6dRxznRUBdsIqbdF65e0+PohDnh+fML+6Rl5SR3s8vv4XDUfIWbG tCTMKWGz3SKOg9YB8TwSxA2gEDSxlLWPoZqJWO8oVXvhBFus/TDmm4sgZwUvQxxbJkhS6T3npMmJ 5gkkUM0HaYRGgacv1uRo0zSDQBjHUUOHTZNVSJCMoKkA7QS3SVK+zog6SdMXCgEeQWPDtNIe9E5v wTK6ekroYRwVeOSMPC1gSEvDXTSbZlnUv2SMo5V15+rvgsAKRC15V++AyEYszzIeGpggIktQRoYN G2HVNdPKzmtdmiZNVlnb/EnG7YhhiEg5YfJaJA4uiuvwmgmDqBHtJnGj3tPb+5Y2dnV+PVsXqJGs rlt9U7UPUvt/6RlnGEe6V/8MWVMWb2HrJFaKBQdhJLA8POpEPR2OmI6T5aIhq1gaag6YLMVyqVjI c+XwUgHk6bEeJTr98lccNi+2Ptj8rnqzQRwirm5ucP/tN7h9f4ewGbFIwZQX5KTJ/GoafxtsNec5 jejm3veY94ouz+1ZK98AHad75pcI0Je0Ja9pTtbvOoIGLupVfu+/yfHvmTO9qU5fP15DXO3zaVKv 03twd30HEqyKagtTbREJVIHFBc3FqTqMtOR1SYAkxk8/fMSfv/sJkhmaJjusaMF6HAUibECgMWet T+EggiCFEOKIUoCcAKIIKWbiKKe/A3Khal6RXB+lKkFT5XrmywKrC0GMOIxACMi2qWqZ7GT1NFxb QM38snYscuBiDLvrszpJRsQQ8PTlAc+PTygpg07IYSfvgpwtkPYtZ01RHoaIzXajhdFEOvYhFk7a ETyRmmyMu1i+IrkyMgcXTkQFCi5KEQzDiCFY3gppxdum+YjD8x5c1FwCcB1f8qqjUlBSMWfYgnEY sdlsNbeGqWZ7Yqv/u6MZt1GgLkLDASRcMidXfbTCcrmgWIbNIQ61eJubGFz6HGLEcjxCkua/cHMQ E0OSggudu4DRfF4cYIjVN2Grj1MdNl0Pc8qMHfx0NS7q5XaBgovmN5BzNvOO80Lbe0TQwnhs6wA1 Widb5let2VJWDuGtIX6mLpIzc29/vKY5Jeemreu1SyuQ17Wi76M3RU69ODtmszqkb7X97/dxDUlT M9X7Ss01Lu3XvkntRULVdJfmBce9+mLkotWA4zBU7ZzYPlt1jQzcXBzGUxh2Nix/0UHdP0hfmdi7 ZesxBsTNBrubG7z79j12724gpGXr0zzX4SBfp+Rxh6Y1PKV1fg5muXylE5d8Kc6Z/Pkae+l363vQ V43fa89UcPH//DcDF0ZU8LIq7W91/FoNxl+C+Nbn39AbUZOAdSKMuGnxFWhBsC4zJ5mjpWkcqqaj R3rkob5K3jKLeQAAIABJREFUUQIHyAIsh4zvfv8nfPr4ABRCLZImqHZ0AJWo6F73ZzQ07ERXwQMA MAKPWFKBCGEYNsipaOEe86bXjcSdFkRBiafnlfreOZAVjzbQ39eaE6QaB/UnSFXdXkpjhkKhZrdr 53SAHOG7ZChmuojmFPjw+QueH5+03oT7caBXrTbmQ6b50CgI4HCcACLsdjvEGKtqtjoEOlFxzYmo ace1FuzzZ/MAImsCndV/8edq4adQ5y2EgCUteH56AnJBEO1rjdXynBQiGCigzAvykpRJb0fQGCyJ Flda73PuppFeWU/OTDupqU8xLmjEQ9dUMQ1YUb8R65OHg4rRjWHQ9NvzNONwPIKIEEMESoHkrP4j s84/W4IxClz5EVndB79nY6am++v3biXabS85RT/f4Qa2LSkaSE1ARFQ1ZA48QwzgGEAckFPCMs+Q krXmjhSDaZ302T+j7cKvomMXr6F2D9hzKqPprhfTFMhJn6X/b3V7RwrddfWWDSCs1g/Qxr8DE/7M 5iwqbb2RmhMcFRHpuhXb8/M0YToc1ewkmrJ9iIOmYc8WwdRRgdqJE77nc7+60sHiCmT/0ldba9V8 WGdDmraNLOcNE7ZXW9zev8PNu1tEDljmGcf5iFRSrdqbLYRVo9Eqlujm1Htrs01OA9e0r633Rh9x tg/Wa+slYHHxoNfBx2vaCz+YCOFf/un/7sDF10n/f+3jPwpctOM1cOERE20yVVMR6rtYqhVd+B4i 2uXKMI6lqmmsiAnBCHgIKDNwfJ7w+//xRxyejgYuBtvUjSlUrY2teAHXQVj7pFPdtkXFceRU4LVF UirgMHTAwiU/AxDmg+CZNN0b3RmvRhfo9Z4VU5gBYyAeLSICYzYqmXrCMnjobDmVzmCby/ppPgvM jMhaTOjLp8/YPz6ZLblUkFXnyebOx4JAyCXDZf2UEpiA7dUOxKH2nZgglufBGQbp7BoQEtPkUjcj 6n/i2ivXYBBriKcn64mxpUuOISKVpABpSpqkCNBKo2xtEdVosCHEkgqWJQEMhM2IMA7mJ6EoaCXJ NvbUVrkz5wrYzOm3StyEGj1ga919dzSHhSapKn2+aADjuEFKCcfpqKW6iVSCFWVQYqadJSk40pwZ 7g9jxJVaVIgDOsB9JqT1yAmm7yn/R03z0RNDMYCkS6o7X4GkpsbnGLHdaKrwtMxYpgmSk2ZS7WiB cwip7wpRTiW5M5POyXFulpW2bDsYIzi/Zx9n2sCg2++le6Ex+4tt6ISV0y8FFVycRZ+snmOgpMuf ImZnFbHsnkWQloRpOqopcEnGdFVQYFt7ZBz4tK20+suvpV65cvb9L3uh7h8iak6ZPkj+TqbFYNTq u9c317i/u8Pt7Q1CDFhKxpItGR5OG0jNT+f0GxPwetBQteHeTvI7nrf/NN7jNVBwep5WgOfrjlMg o+DiH//rb8VTuNVQp/8YePGL7ELdccqI+I17XEJ05891s0ZDjuilvWruMFBRw0A1GZbAfRgCiDUM i1zetQXVYu+LTWhzz6SieS4+//wFf/jXf0NZBJLVJNI6bv+Jr1BnCG4+6MNRUZEvu1nFiYU0L/gQ o3k/A2JaCzEwU+pzGFK6QmRQRywt/qSMVQBkMDIRECPCOIIQkEuu2gkp2SkRvBCZkyiXIvtFXqX4 jiGMUSvAfvn0CfunZzMn+DXGNNFMHCLSalLYs5nIfEcKQggYt1fgGKxvRe351DmRks2kAKBSw+m8 4JPPkTPFok4pVcp35jaOA2IMmgcjaP8P+z2W5yMkZVDUcvHVtAACSQEVmBQtWJYJc9Zwv7jZIA6W AK6ggYtVpl1qIqmBBl3Opn1C0SyVticqU7TInCJSQ2ZDCKqZcpOE7adgjp9zSjgcj5CcEc3Uwa5h s/TRuWRzTh2r/d3n2KujVrYlHaDo+lGje0DVpAJpIbr6tibhVSNS56XlDclSQAb+hmFAIMIyL1jm qUmc0paZa5hqVeAThngWMvqK1Pey2bYB21PTyDnp6r7rXj6Q5F+svn+BZlZw2t2w3stuWDVLRn/a o+DEjrw/AqOXsBD2FrZKAmwG9XmpIFJUE1q1fNI3RUHrmY7jV7KvXhYpFWt4OG6bF4IlcoP3Tx2D t9st3n/zDd5/+w3G3RZTWnCcjlbHpoFf/yFbvh6uk+L79XJHLpkjVt9ri1fXv3jtBV7owOUtEHZm SnGeaZ/Cv/zjP/zWM/v9rcDF1wzarzn4jfu8LT10g4eVP7i93HJm9UN6EwgYIgFE0c5HiwiI9TrN bqlESIhquJ87p3EgSBKwBPzw3Y/4/rsfgaIFxtjV3i5B+LQBgDS82tdwsM7VvlVmWzo0TgCIa22N DKUZGeZQKC0ChcQ+O5gg1WwUAoq4EyJjKUAigIYRw2ZbCQQsMgFF642QqVCr42V9a/MkJFUaE+im liIYhgFpXvDpw0fMx0mdBGuf7bcOVOwLLQhXjBE6aFJJcckJm921FjljxpJz9SkBkTmqObGWOppS wQV1pc5hDqsNnGiEg/pfKPPSME7P5bFMM47umGo1SVT6ZwUxuaivBysxymIF4VDAlmQrWtpumCr6 LIMSoRI4sX6TF0cTZxYdCDGm0G0NI/yaIjwETzwvllhI7H4anpqXZONG9f6BGJCClBPEUtCHEDBY n2sBb/IQxuY6W6fW5mO1M8XXuM6PV0u9fNi9DMh4v3x9FVG/nnEcAAA5ZSzLbKa1bi/V4aVKlx0E vHa8DCTOzzmjJVttpwzI8VaPfhRk9d21ufVrnZD0zzw7gx4p1Pv20Sa+F+p2Zc982+aqMlS/FgbU wSgpY5k0701J6tfDIZhW0jRagsaQva0nIeYE6gTL18f+1cPXeA9sAbj2oLukawsaoIUKabuba1zf 3mJ3daVCW9Z06X1FYgfQqDSoAWq/5sVmdmtgzegvmBBPrr/0vhqArwAXa6DRMlf7NIV//qd/+K1z KGej62H72x1fCyxOVYxn9qA37vfmhEljBnI20Kp9cOW4mkE6cFGBRQcoKIBosHMDiCNyFisIZb8l BigqYQOBKWA+ZvzxD9/j409fzHSg5gunl64q1wQ/BAiv5rwqcLtoFoH7Diix6pKGA+JAQtTvwPrv mpAaJSJ2D6BWNClQdXImglDEIoxZBJnV6SluduoUmYuGdAnUBpszvDZLD3QqXjIQ5WRKjLAFjhAp iMzYPz/jy6dPakpwh8oOiDS04lTfJVfjm+LOmOqISiFo4p/NqFlNrWy7hqd2BikC3E+G68rgqunq 1akavmnMnNUkw5Z3oZalNo3G8WmvoZt2n2raEEGVkwTKEJmRpWBOWos1csBms0XklpyqqrENJKiJ xg0yvvCb5oEMeTZsYT12DYoB4mZ24CrBZRtUIVSzz+IZG0EGdjyjpt4zLQuOxwNQCrbbLSjGqtIn QMt6h2AaJvObsDEPzsylZ0DkE7vSXuhpn/vS9x6ucvYCfB6RBALiMKjphhnLnLRQl7RlBQNn6Aj7 a8db9urXf4wGuk/oX3fJ6vrWzo5Vdry3ahggPmyvMLcTzcxJXnGlBS1R1qlrdb8jK+gAQVJGmhcc 9ntMx0n3tuVQ4V448K5L0wC0KXdt8zlo+iWH06FTobLXkZB1/exvMq0xAcKEYTPi7v4O3/7dt9hd XSGVrBlMvTpxvXc/KjDa8gJ/ewuUdn9d0i74+fPP3a+/Yk2+aGpx7vjP//R//Xa96PrV+Lc9/hKt xeuD+xVA4vRQl2UA1BI6obd1tXfqtBaEqC/Sc4H9uwChAOYBIC1xToh4PhxxPC4QYjCP+h2i2SgF zBHPDzN+/6//hueHo9X1YOTcNbW2zfc4Of9viL5Kcp3GohNQ+20oMO1DWZMBN424YJsLoP4XqqEo ICRo6GomxiyMhRglRPB2i+HqCnGzAaBSOIqY7TVXyV4INcW4M29tq9Q2245DKSoZw8JRHx8e8PD5 MyIHdeg8GSH/XZM6Oqs5tafVnCBZtQDb6x14iJhzRpai0nZZgwuQ1OJpCgpRHVfZQyM7Qqe+IqH6 lYSgOSu8ryEwpucjDoeDqlHJJ0QRXbB05+4jQKzMMucFkjMkF3VMHQaEEG1czbPEo5iclFUJyfri IXc20S19djMjaL9Ua5Rt/ogs+2jVWhDAGq4XmDFPM+Z5VlMfOlJqwDGnhHmy5FsxIIyaH8UBFRmQ csm1VJVbi2gg2weGzvUpfeRBldK9H20+qm09dGmaq9kEAAfEOCAOIwTA4XDUsGLxvSYGZDqw8gIN fctE8tr5qp2Qc4Bx/qCTP3ygnEPL+mJZy+j+g/M2aCfgWhLpOH4PZgvMcfZk3F06BzSBmTS7K7Rg X8E0TTgcj0hzBjNjHDeIIViSOEKtVn3KcMnNeH85sOhHCfB5aACgBxOrcemGS4isJo9q4KKlEL+9 u8P1zTXCMCDnhGmaMS/J9pRFTa0A02Vw0dp1+fA9fgmUnGrnLwKMF373EsBZPddfIAMX9rCGzf72 x68xh5wjp1/zXI8DILiU4I6YLVdFV8WUAgjmU8GDOSXpYlHPfQJRANMIwoBSCPMs+Pj5CV8eD5hn QS6MgqjX0AARQuANvnx8wu/+53eYjwWej6IYYRFb4e5Ipm+2aa2dNWSsggrXOggs2XP7zsBU9fq2 7yDupCn197CNUIgMWAgSrGQ5M2YwMGwQdjuM19cYrjRUshRV4Ws2QwBSzKlSn5+NMXubtWENybtE XJNpmXr98fMXPD08YIyD+QP0mwZrYFGZawvZc/BhU46cE8CMzdUWcberIbYsBOrBBWAStIKK4EDP 2goHGtxUhu7B5epLZgUXWkJew1Lnacbz8zOWtLT9aESYTUtQpKhvgD2LBEAWpLRY3Y+AYRwQIiOJ OjE64T9X1zcGxNTAROmAmpio7um6nVe5pkhDbjVXRT/O6icCpHnGMs11bzoOkP5TKaoWD5pufIhR gZSUluY7NPOIak+kEVJ0e4I7lbOj4mqzl1Xyx6oXMxNMpYMECDFSUUk8DiPiMGjl1aKRVVIlUL0h W7rwS2Goaz+Jy8S5/t0xs7aMT4n6S5TOmH9HG3r6QOv/6jmfk9M7n+smnB5019U2CtzM3jThTUvk YaU9FKvqdPtXckZaEuYpYXHQGVj9iryke22N3kW1fz1YfGFovuI43R11Lk6Hvnu1oIj2uYZ12ysO A27e3eL+/T02ux1yKZiXhJw1826WYlq65rfg4/MaUz89dPm/fO0l0LL+jt5koq9qU+z54Z//3//z t67SZ1Ov/zrc9/XHLxmw1+5xdu6F53zd7wli+SnatmxJrqq2wswY+lnNIMQRansSG0eNsFBTyIhc GNMk+PKwx5eHCfv9gnkWHI8Z01yQEyNnAnMEJODHP3/CH//wPaZj1hwUpTax8d3KHI1dnuwA3+b1 B+IM/ITw9cCixrH3wMLACZQIqxMbIYlgESCRoDAhhwjsrsG7awzX1xh2WyAOmvvCciVQ0SAIKcUy SmoCqKLUGU4ymnRg40n+SY+B1bH06dMXHJ/3iMF9KYwBnS+E9edKHfXebONYStGEWsOAcWdqehGt u+HtMgJCxpAZQBDAPT6K/e+qWpesiGDRFqa+JcJgIZlK3XWcnp6etE6KPY/F3plRxP1ATA6VxsSl FMzLggKt5xHHAYgKRpI5rbpJoI9qkNoxm19pTKStJobnhfA9JQYwAFgEyVAnSKy0/BAHlJRw2B9q tswawmpmItKB19wSKUEAbMcNNsNg0TIn0TNo0UrowIX4WiYnsL7SxbKGntijOw0FAVU75BEL6lAL M5dFhDhgHEZAgOl4RFp0jpg7X5y63Nbr7aslT39VSXL9+6Zl6EDQ2V1UreJsu1MwOClrJ7ofNxDS qF9zZPR2oJrMVhqSjvxULkINjJv+QkEhmYrJNMKuSfVfM1i1GMcJx8MRaVkAUifiYJV612Dil/GR V/1hTm7jnOASw+3volpjHYcibZ0HLxYoBSFGXF1f4937e9zfv8dmt0ORgiUtyBaGCxe+sF4nL7X5 jNF7e0++O/3Na5+/Bly82g64WcQYqGePPCPK/8mOSwPW+2G81Py3pAU9XHPRJ8MKBjb8fDBtxKB+ FRwBirZRbAuRM2ozeyDgeEh4fJ7x+dMzprlAJKKUiGnOOBwLjoeM/X5CAbDMGT/+8BEffv6CvAgg ASVJy0KovW6OmxcJDFcEUkNJO7DU2xQbgAAgzRmrEgVZpwEvok6fag5hSAjgcQDGDYbbO2C7A282 wBCRRVNU56I5KBjQFN1Q3wOAkElQOuLZajSgMoh+XQ7DgN1mg+PhiC8fP1lNESfcvGp7f/i4OTNZ 9bID7bkUZCaEccS43QAU1ByD7ncdIWUAsRqrVS3sJNoL1rlZwrUAgDLWaJEJtU6ICB4eHjEdjhVY BAMPIEKSDOHeV0MZp4ObKS1IHvEwjojjWKNO3AnYW6BSP+oJ3z/6zqY1OAHoHUPXjKulmkdi4JWy KYAxhAASTaLkDm0KUN33wVaj7WGV5hSYxGFAiBFgK9SmjQPIzHWncjU102BTD3t7TmjDyifD/HSK tqOaRywteCHAtZeb7QaBGHmZkeYZkpPOpZsCau/bcS4dvnJcYiKXfiPt7WsEwotPfeHkufR+wkil a4BrKPycjSd1JhH9xtZfJ8BKBYgAqI/2gUY8lYKUEvaHPQ5HjbhgJq1HY75D+tN/J2DR9Xf1aih1 PQ4AahbRLn13TYFu6edBmlfFrxnGweqUfIOb21sIgOM0YUkJgIe6W9/Q3vu/UYHE+p/TsF9iSll9 T+vfvqSleE1zcQFcGKNpK+WvcryUue71WPAeel/+nf5YwKu2W586TqUL3CW/RmaxAg/rTJoOvMiS YZH5UbABC5fqtC3G8jNBnT0HpER4fJrw+fMTjlNCSgAkoiAiZUIpASkJDtOC58OEJQm+PByxLIQQ Nghxg1RgORcsFTQROtJcJY4mQ52/dATNJFKZhYMGqrShkgXzqxBw9a/IYC2hzgElBGAYELdbDNdX GK5vILsr5GGABLcjmnRbtGImQf0UUKUY1KgTYrYCWv0Cxpq4kuB6t8PN9RUePn3Cpw8fzcziM667 S5z5yAkL8g3hoY5Sg1zhWpICUp+HIWJzfaUVPXNLI60YrIGUAEE05NVCi00LVqUy34gqyQjcX8Ec Fj0dNgRfvjxg3h9A2WqVGOHQkuaeQ6BJbJZZRZkugFQSchGVtLcbzYTZVqZJ8GIru0UgkCHX4Amm shFHa7t4cTRx3qvzIl42nbVya09R1AFXezBNxxoRUIoSUubm5e8RRXlJVVodhgFhiKax6NatM6nS okJcw1UdQqkBKFcVt9wf7RBC1ZTllDQslhliWTtrxJER9CFqjZiSNF9DsXweTD6iaBouX5ongs9Z 7cNVgxqAq2DtTALt0PD6xyfvtP6qDd/6RHfNJep/NmYOIuw35O02sNvT5V7ruAJDVRPTbXHzFm/7 WJByxjzPxoAXle49oqQKE19zyOXxRpuvU6q56v8FgLGeA1KgD0ug5zQOtHbwBkDM2G53uHn3Dlc3 1wiD+l7llFCSaWDpBEQYjXeNaPt7rTU4WxY9oOv69WvMIi9+BwcX//gPv10tUgLeSn/9lxxv2Rv7 7xlcB6m327VBWa9UH3SNJhMQFuuDkX0JlUG66qp/qXouAlBzxEpVXJ/lphLVRDDMFOL+F8ohTdNt 9yyEnArmueD5ecLDwwHPz0f1nSiMVAhZ9FWsLPmCgEMSTBIwlQBsrhG2N6DtNTDugM0OJQxAGJEp KsDoK5gSa00HAztUiR2BrFw7eQGvutN7aRoGlDSFeBZGAiNTQKKIHAYsISJvtsD2Gnx1Bb65QbjW v8tmi4UjSlA1fykFkhMkJQRotUyvIFn9uGDAxjdK6ZA/xPy4bE4kYxwj7t/dIhDw459/wMOnzwgU NX+HlUCvNl3ngt1RzSbGMIrtxspAbP0V06oMW5X+i2kgcslVdaHjq8QkVIdF/d8TcTVibQQiMESy rlkmqK9FQIwBrjw+HPY4Pj2DctHcEA6GLUSij6ZwYlIZr5mLypKAkhFC1GiHYdDqrpbIiwQ1vZve TXX7leAYYXfpUqDJs1oEijQfGdN8FSlWAZNrWLAnDWNWsHI8TpCcbe9yTTHuBDcIQLkgzTPSsoDI IjaiJnirBFY6AFPcV6ibZx9/+5sdKBigraZLG7u6xhyYGngLTKgGRgIEll10HCAELGlBWhJQlHYF 904WqJMrETJUy+hRMrTSghmlq4wDBvQa/akRPzYfbs/vGYkCGqm/B3wuT8fFD2uj+O/p7Dp37l0z ZaebbZwEDWw0TYVL2j29JpxG6rRGleroTNYw12oQlL7mOWE+zFi8nDsYkWM1gbk5q09l7/xSIBBy ENq9QGp27NoidQAuA7oL+oL1J7OZrsyitS8wwUa1dyEGXN9e4/b9PW7ubhHjgGVRZ88Mqe1ZaQ2c qvdt7BBRr4n15jjARV1njZtWfuu01jRIrXdAtcNTN68df/Fr3ARsoai2FIhw5gb7VzheRz0ErTmp TLqCC2kqpyrVSuu8O6IRCkBa4hmICig8qZVtZeGOWAMARTANgESIKNpsTSQoxQW0VFVEDCM8QgTG iOtge5hjJixTwcPDHh8+PODTpyccjkmBTlF2VMRKkouaF5Jo4qmFIqYcsNAAiRtgcwVsdgi7G/D2 BmGrPg087oA4goYNEI2hW3bJbK++bLrUc1hdo+dVK1HAyMLICEiISDygxAFl2KBsdyibHWR3Bbq5 Bd3eQq6vgd0VZLtDigMmIiSrJAgRTfU9z0BaEABEUsLqTF/EtBY+98QtD4ZLqqBmF6eCd++u8e7m GvvnR/zw3Z9weNoj0ICAqKYXGCHpgMUKoXPLMCq2mWopcaONzAzkjJwyKEbE3Ra0GYEYkFNGF5ID smqvUSxQwcdcxKq++oZuElZzOlOpO0SryGl+GGma8fzli4KLoqakTKI5RKpU6KpnB0msQEkEAQwu yqBFRPNfbDeQwE3yL2K+HM5G9MWVmAEu8eSsoa5exIu6ftgAq8bBEnC5k6plmq+F30IImPcHLPMC CBDZCldVx0hUQIWiDqopq4Yjxog4aB4MFyoCh1aBNrcd63RipdR0zYW33cetoz4gVbl7NVUGIRJr yKuvK4imWo+MMEQLLS/IcwGy+t6Qx2oTQ9w66U8xhu5+NI2Nk9ZXcSbpAO+UEfdHu6wyL5+ztVTa y6t+ffebfsx60No5Q1RfD5NKXBvXzB0nAlnP3YCOw7VxgFTxorZTSODBrG28FExSIZQlYznOmPYT lmmGZA3BjiGalqlnpG3OdP32Y0ZG3TuJ35rdzG/9qJ0NPU5ZcC1MeQHQdSPRnVS+xjFgvNrg5u4W d+/vcXV9DSHguMyY06LcbIiKB0VBqmofqc53Eam+KKj9p5PxaOfQPta518sUXPStdgHeTY0KTtYF EZtmRU/Fvq/NXvi3P5otGvBt3P7Wd7fD66J3QuEoFwpAQAC28Kn3bcGGwAUAlZaRTwmrJ8Niy8LD J5vEU3lrVIhqOVTaJmuQSndAyYKcGcfjgsfHPR4e93h6mrEkAoUN4jBAiJURiEubbGF9AlMUryQC IgJCBFgQYgFvr4GSUfICSQskzUhpRl4WICeUadIcBB7CaPdye35vJvBF5gyHKJhjZQCFAIojaBgg wwAMI8BRfSvigBTUFi3qPacAoQiCEEgKkLPao3Ou4ZRFqOaK6NyWdIFXZAxkaM4IYrOmSMbAEZvN DjdXV4AUPD8/4XA8dNJ1rnPWS3r92vK/pX6HKrn7jOfSyqqnecb+8RHj3TtsdjtL6BQhJZu/SAMo TWKTBlzQxtaP3rRSmbIzM5f6Y6zFwcieYSR9xQ/qnaq2Rn0ghqCatnnJ2D88Im43uB4iws0O23HE AmDJYqaJJp1Q1z6AVJMQGMFAVbbwXwhqBIdLTpI1V8iyJMzzYjZxbUchAg8DttfXuLm/w3Q4YklH gILlyyhgaEQRwRIs2XPmpz0+W/Xde/478E4LzJHnviBGzAWSxMpcd+2nRvr8dM0EqmV9W7/tr1yc HBekZcFC1IqsiTK/XAqECMN2i7tvvgGngi8JOBogDJZvodh8sWV5RY1makdP7fyPVwHFhbV0dm5F T09u3j+70zx5n/XzCecBmjIYhu9WSRfFImcYhfSdXEsg5kMFFerYzM9CRWWzQtXZVmqVWwaXUN3Y FNMyyBBZSQXH5yPmecJhf8A8HXH/7Xtsr3egUaNKlOFmq04MS8Qm6z3k/fE93K99G7LV/KxH72Ro nYa+dP35Ufc0AAqMYbfFN38/4u7dPb75zW/wr//f/48//M/f4enTFw1bhVX1FV26ZHOoOMXMltTm df0s1+ae0ySnmj3iWNMCu+4VxcDq+1Nw8Vc9/KEXNsP5cQosAHc4bYoVJ4YEc5PXQVPrt15P2R6d u98FoBiQMAYPuNOdbxR9NflMfSrIkmJVx0gCvIhZzmoXXOaCxy8znh6PeHo+YJ4zcglIiSEJmJcF IRIKq0q5cOc3QZaBwcWcIjB5sWoexJF5EGX4IoCo+ptyBnIGF/0sKa+SHTUG3wCGWhGopXCmAIqD lbAOQAiQECEcIRyqrwdYtS5izXW1XCACLQUkGSgJbCm+qwup65xXa9QAkO1mCsHAglEzApgjttst rq83iDFimo54/PKIZU4axglomKvVJ9FbNmCxIqKyJt0vEWjXXhz3BxyenzHc3oLHUW3tRZCz+Wso Hq0E3SvDqhp23U8nJurXYdNsbc9ZzSMVXISApMsAwQCsOMrwPWDnyT+KtEyoUAknTTO+fPgEGQbc jQOGjVaqlRiwFKlEytecpbmskgpbUjEpWlXUgUzVNJwQnJQ0hp9DxDC6f4jObxwibu7e4fnxCXlJ BsKqqzAERbVvthdIAJkTjukZTAq43v2XvwPvNOdEdsYxREguWOayzjR6ptbW9oYQVHtV2HKtoK4L F1jtJbngAAAgAElEQVRE1HfFZbiIERT0Xn20Sxw3uL2/R54WLJMWq0KxPeW0qZpBGoWRuk5Rn6/j +TaNXIOC9Tk/f7r2K/PA5TXf0EPPNA2+OoA0yZU7Rr26NakGqnQMjqgTDO3WAXaNUz9vq4VjixQI i2qwSKB+bmjr3P7OqeDx4RHzMmF/OODum3vc3L/D5mqnBehI6VRV1hVXb3Vj1TVfTEjogUXfvreO mhvlrWtweb6IGQiE8Srif/s//ndsr3a4++Y9/vivv8NPf/oe8/6gZDyraa4msoNqAR2oV3ru2lvp 5n6VHr4Xxtzc+nUA4sXxsDH8ReDiJUfMrznqr84W/Mkz0HfeFqf4IHj8ryHl3sbXIU6RUAeUSKpk ynVHK3KG9BkU0VauPUuRGxkKd7c6N7WgXgeI5nhAQSqCx+cjnp6O2B8mKxAWLfGUPi0dJ2Rn3JZw SJuj7SxFM2RqbQtTwxKZ+YC6XA+2wziq8iUqkWYIpCQllvbMVXx/x1yr1qeqjFUSdCdLV+dX8wRQ AVn93/c/qWMj5QQW1VawbTYtWgUAXsCn31io0kQVIAhWgKsgxgFX2y2urnbYbiJKLnh63uPp6Rkl Z1WtF606GCPV+xG9sAF8w9WPslqT7ldAFkY2pYzD8x7bwwG7OCAyI7Fu4JzFUlfYvEDU6bLTjlQt ClDXTZUgRABhrbOQEkLUqqIUdA1nD/OEEYDa9M5GaqPpfWVmpJQgpGmIqRQcn57AP0eM44Dbb79B GAKGcdRw4iVpCK/5DPn09EQwhIASCvp02i4x9aYRAiwHhGayDFa11sEWgTBsN3j3/g55nnB4fFbw SQQpWjVS2PpaNNV5LADngvT4jM8lg4eIHd0j7DbmH6EMaRgVbM95tgJsRYWCjrn2NmsHGCKiFTq9 Lw6GbGC1BsYCEULcDJqHw4SKZFnthqsrvPv2W5SS8fBzQTrsQSWbyl1BqkehuD+BA64GaL6Otp4S 98tA4mWA0abrJYZZkYI/sUrvBMdKxQOgVIO7apPTJWrCiwtvZhOiYoDFaTo1E0URWHI4rHzTVcso lU6JEBA0k+rz4zPmacb+eY93z8+4++Y9bt+9w2a70TVpCb3IdpJ2Ruoce/4Z72/7/7Xj3M/HBuFr flz73X7W6FBmgIaA93//G1y/u8Vv/v6/4Lvf/QF//N3v8eHPPyIdJhV5OWgtIO+ZEPr6VYDTCKoa lQ4SdkIWORGBQ+DXwNRrwRe+hzqfi24RXLjJSwPySx/82n2aI6VPumP9tf2neaG7xOPqWf1NES8m JrZgvRaIgQqr7dFHWEh3NZGDBgUWLSxVy2QTO1FCZbiaLEkl7ECDMmlTE0OoJmISsTDHUpCLaBpw aQ5zAiBJUa0AaVSImFo5U0BhM6lAnUBVmxCQKSAjIBMjw50wIzJHlDDUl34e22d7FdbvclA/i0wB BUGLj2Edwqo5FQgB6nzHAnARhAzEIghpRigZLIIg5pBYNSeWdZLMoYlQU477ws9iNm2rKbHZbXF9 daU1HkQgJePzx4/48NMHLMcJBM0YiQKLuGhs3NdVL0UJNYJ26QghqoRua3ARBQzjdourq+uWFdO0 K0xAYPXAQamWabjmoq6RKuQZiOsYte5HRhg0hj8tCx4+fETaH9U2b4mzVBp2z44aw9Td159hDMWk QJee85Kwu7pGGCN4iCik0rmUDCquiUAljivJ2N693b2P/qU9T2SmixBqbgp31hvjgLTM2D8/K4hw cNGFnJKtK19rAsGSFixZi7qN242WTbdnBV9TKE0bYeMjXZsanm6ZKaqjZD2vvwx2TUkZArWNs+dm qFBR6wGFEDBsBqS8YF4mTeFe1B8ggEBm8nGmVjx0kfxp63V7eqzW8Gt0+YV1vT79Fm0GALl8K2mr rIJd6b6r6jpa9aZCbLt3vydqi84+62Koe6eCaapaPMUx6tiyzItWWjWfnmDFxNzJmNZ3X2sWHcSg 7am3jovDQy9Ogf6GLMvtJeZN0LBxKF0MMSAOA65urnF3f4/NbossgiUnLEkTcHlqfE9o19rvdNbe u38KNqyh1HUc68avolBO8MFKM9Wfh5lTe3Dxtccv0V686rx54Ts91Rx8PNoBaERBDw/NsGtNT1bt T6txU4YipKm4BYwkWskTbNoDR6uE1YJpDqMqrZC9vD6Hqoet4BgHxBCwGTfYbgdsNgOGMQDQTJSe LhlAY07muS9FzReqrUBN1+wpYd3ZUsmPftbMoYYyfTEJwMIaESKmnbFU3VIInjHUE9gIwuqaIqzm EDG2JV7N0kGEvkIpCFkQS0EoBZwzQi4YSgHnBVyyMgaoMyKZuk6ROau2htwuqjkzAPesVyI+DCM2 2y2utlsM46DIu2hWxI8//oxPHz8p0S9QxkguUTZsfmkzyIVz/d9MWhpdRKr0lVEQhhG7qytwsOJa Umrui2C1LtyG3wMMGGP0vhaffy9D7cXoQBiGQRlsSnj88BHz4aCFyshygtQMmL4+YSpnqoRSRKMz ajpuJ7JCWJYZcRjBMYI3gyY+88RWAgWBtmcqi+gJCzVw0X9f3309FtsfpA6SHDxnjAoFwzBAipqc soUWkmkwCimY8tweGiBDlelPy6z+KZZ8LHQAo5Vpl5qfo06I96XubcYpDaogyT6b/2qXwM6jYJqW iMicd2GaoqCAbZ5mlKT+F5q3wFYF2dqgpg0AUCMW3I/r0vFLBbZ6Hqe3lJPvaNXvUwbrmpXTuwtg HN8p0/qhlZ5Kd72PnvR38Qk6l6lru6S/lCrIKgZO3Rk854xlXjBPk4YzCzCECA7cwEm/gay37daX enp+dD+tL9eEvAUuas87UN6YNNX6JMQaaQQQNrsNbu/e4d39HTbbbTU/5pKVH5FHZWm+nBWN6NsM Ws8ztT/EPnNtZ2OkXtzP+WylE/05UA3xD//8T//w27fCRF8bnF9z7eXvDDBUAtchLUebFdfV2M9u ps0ZDGtg4aGUiSISIpbCyIiAFRLTe9gGqQHorZ0OKlxKdhtV/1xtnCBQwWZg7LYDtrsRm82oEilg myEbIJAqMRWTLEsulbE5QbtkZwcUjPShYmQEIECzrXIfTWP6Hu7k3arREbOJmkd2C0PyBE5AEEIQ QRBBFAGXUrUTnDMoJXDKCJJBJQGSqyMXuToUjdnCpOpCFqYnsARIDFh9ie1ug91uh3EYdF1IBgMo y4KffvwRj1++6BhZhsrIsVsf6zW2lvrO11+/uUuRuuk1WQ8hi4BIpeVxs1PQUedSEMggROlUrT4r 1KISil8j5hQIIIZomgvCuBmVCOaMh48fsTwfVP1pYKav3kmegdHNe3TaL10XdS2Y49yyJEhghN0G NGo+ByINpS0517b3KvWVX06tnOfb9ETjKWwaOvU9IaKaVdFNTtEAgTKBAyQnXbfMqm0Baohk0y80 BjAvM9KSEDjomHEwi6eG9woaCIIJAjjph/ex958ovvd8HAXod1HREq4qaFgEjG9Li4XHsBkAInWA TQmSsoWeSo0CKcAqiqQBC3QU//z4WvqsJOIUQLTvqI5vo6tUUU/XFnGo3umBVnN9AgKo01a8QN7b u4OR84dW+op1W/xVBTv7mdIBUTAIDXmepxnzNGGZFqSUbR3GmnPFuaKSvo5xnkGr1rc6vr30upJk O8b7xtHvrfMhIqMxPgBkPksDrm9vcHd/j9t3t4hjxGLVetUhPFgkic1t15/VtJyzzdY3tP6tBQsH GXzxPLq9VMHFpQF86fj3AhYvfk9OhvtJpgrznVER+QJs6l+YaixiABtRL0Sm7h+RsMEsW+yXiKcj Yz8RCgaEaJ7gyGb3pTrovRqVrfZAb4Vh1rwSKq0ZipYCJiUkQxyw2+1wtb1CYEbJojZuoGosHGWy LQTJGUgFlIsSpVJUqpPm/aCioQWTUgHINANQ7UchVPWyq18L6XnNhqmEP2P9NwgWzWHEUCyQVwpI Cqho/VM1dWhSLJQFYqp1lKxSpk+ngyFbfKo1YHVMNQ1QhiALQQKDQsCw22IYR82E6LVaIJC8YBMH LMcJ3//pTzg8H/T+pY8AaCDmktrO7bUrQnByeMEu2PyK3ZdCBDFhvL4BDWo6yTmZ1OT1UszXAw73 dBQ0qgYrU1ox4sIWGktEGIYICoySFjx8+ITFzCIOLrzdIlab13FwHfDW114KJxHVJBFpBk8U8GbE uFPHN9VOCSSXljGxzuHJnnUTgrTkWqvD0JWg5SkI0dI2k54P5ntQUsZ0OCJNM4YQKtOB358aw/KZ 1eyY6tNB6JJsWXZQJrboA7LMsBpFFELASpAy+tLXYSgm+RZCV8TNhQuPNDK6xKYpse47k+LIOo9Q NX2el5pYCbC8KnZt5R3ozUyv0M213NOZ9309d9oB8eVyYY66J9FqAb120Pmnbq9re/zp3X57xXZ/ fly61oQrT0+vK8f2WpP2G0Nta7TkgnmeMR0n5JQROGAYBtXsuaBhg9jXAOJXxuOrBOY3hvOS3+Fq nE5BB9XUKUqDtiPe3d3h7v09xs0GS0rYH4+YFw09DwaiBQ0mOZD0UFZ2kOWCFHvae7ua1iBiNdYX XuvEk1Zy/ZI95c3Be+Oatybgxe+pGHgAPNbWtnhFuRVYGPMEharKJ4mIGACYD4IBi1lGPM8RD4eA L8+ML0/A075gTuZABNH6AIEM/aoWoJpDQDb4lgcDVpQMpJKjNM0ABGCKgDCWWSNI0iJY5oy0FGhK 8NCc8SxDoce/BzA4m5pYlOgryDDgJRkiGSQGLJChQf5JwUUFFqJhc1TsJSiKUCBsoIIEQgWFCsTH viiYYMumSf4se0lJIANiAm2LiJt9khXekYq2K1QhqrkYhFhzNkD/RoiaqnqzwTCOiEPEaKpz1wwg F2zigP3jM77/7k/IszINKYIQI0pKK4HvFFw0swiffe9HNdvoFx1DAdjSQO9ubzBsNhrhUZJVRZVu fTqQaFZ5AaGY7djzXwCmVjf7K5GaEEIMyMuMh4+fkPaTAjoiKzXe2spGTJtbMjdpw57r/zNU++S6 oyQFEghxM2IYRwQmsPlclKp9ab9fSVluzqt+DU3TU1WjDiZFF3UPLnSMNB6fBEgmYergaHRR7aWt n75fbJoqlIKcErIIOEaEcWg+GHBzU3PWZJ/LCrqa4OBtrxoPV/UL6gg7M3Vg2ObPZG1jUsJaJn4I A6gAy/EISamaqeq6sAd32mZfQXjtWLFzaY6gvcZqbRa8TG9d4HTG3M6fgtOmM66TcvIXWVuknAPO M3DRMTDpv+8Eg5WQcPLzJlSi4g4fB/JmSstfofV2ZsyTpp/PliE2xoghxspftGm88iW6dHyVgP3G JZfno2PiVZj2PttvXHMJAseA7W6Hd3d3uL69AY8DlmLaMlN5uE8Ph87nRNRp3U2IPSirMOQkz8Vb r/567QN+uc/FL9Fc/EXHyhmvgYs6W8YwyZEmR3hCKpEIwgAqGxQMSDRikQGHNODLnvDhEfj0KHg+ MKYlYkmMZS44TjOWpYBJSytz9cEwZGegQjNfWjM8uFLYRBG2fxFlYeSFcTxkPD3OeHw84vlxwv6Q kJaimhLW8L4QAtjUq2IOj9Gk8DZ1TuTUh6GkGQxl/kESqCRQWRBKRjBvDPW016gNQkYwzQNLUU1E yQogSl7/LQWxqNlD72svyaCSTVuRTHPikR8CsXjyXHIlwl6fwf1GhBgSGKkUTWHNAYgDeBgRNgos 4jCqvT1GSwutDIekYIwDAhg//fADPvz0Qa1iBi6YgxK3kzV6Ciyq2vYEWKx+02WMhDFA9UVQQLS5 ucGwHWsYZSBYumMtTuTM2LUyyozUHSOXok68vsLN2aw6STJhGAdkd+g8NHDhPiB1K3RkX8fZyaLt ocq0qAIRJ0yFCUtJKAB22x3GOKp0HQKytPatzCL9GLpZx5mbMaq6V729zjgMQLukr9KTmUGyYJ5m LNPRslxaNKwzcgOpfn8SdZJkENKyYJrV7kxDQBxidfhWYqljomDItZI987O2dwzaTTfZqsJWUND1 vYY0kz2ny7gIaJTPGEdsxw3SvOB4PHSAs0USeTv7VOk+ny8dp+v7pe9eu3b199kD2tnmcNp8epSB vNyuqq2sV7vI3QOV9uD1rRqYoAo69L/TGIdmLm4aOgeF0l0qkKqR8oiS6XjUyp1Og7t2EXo21AEq +lr+R90YXnh1mS4vf9+143yA9KdW9VcYGDcj3n/7Hu//7ltc315rtNiyIKWklZODZxx2GsOrsWfX ypUKSV/UWrz1crMIu4P7f67jwiRUgiVVIvKJKcIKLBBBNBqoiMhCOM6Mxynj87HgaQb2S8Ccol6L AEYBCkNmgeSMvBQc9gn3txGbMRiTL8rEWEN8nDAV8yNVG2pEKTAtRcLxkDDPGdNxwbIkFHWnQEpA zmyEVtM9c4iIUhBLwRLZ8mUY6qzJfGwEjNFJUZdPJb5WTrtqChgJgpbb39IXk/oN1JsBpjpcjzyg AbcQoEYZoFfwQwEeuraxgqtS1F6/lKJAQvS7Ams8MxCC3iVEUAwIw0YXP8fm4GYLNaWEIBnICZEZ wxiwHGZ8+vQZaUlgCbXNpgWv5oL+OI1kaFqwxjzXakpnal7CTk1SJSekmbFMR+R87dTMeHmpDBem veqdN0spyNLARS061hErJ4ACqyCb+4RQ/d5wqc5JLhmjs54VrW1SJWBzDO7DAjkL5ucDjuEzpusb bKI6kiIQQtYaByklJUimLnUJkVlLogfOFYg0qUf/7k0+RTLSvGC2z8MwdAWoIrY317h+d4Pj896i Vqzom/ia9l4qUAgCoGQwMQZhHI8znj5+0ggjCG5u7xCHQdkFA+MwQkQwz7OmIre2ijMBQfMNCQHM mnwp56zKFB9lj1ywWcglQ2Z95kCqNanfUwSYNUT1N79BSgueP35CyuonwnBgJrbXelDxOgN7LZS/ 1MxKp9e6meT8d6u9TM0E1WswXEvg92ptOW+XFrs7pdUAEACWdp0/w16eQ4NIx7oQ674z7SpLMMHS mR9rLgzzG4PoOU22pXOl2t9mzk7Ton4+84zj8Yh3D4+4++Yet3e3GDajVdrNFUi0aVmb+d4EGfL2 HOKF+ejp7frs+vbiIJ8EcQy4+8032N7s8O033+L7b/+IP/3hj/jw40+Yj5OtNW4Rt6WjlQ4KuYGE LpDt7HhJ67J+/0+pubD/xHNRBJBY2Sb/jgFNgMVIhVEwgvgKoA1SiZgL4+FY8OFLwoeHgodnwnEZ kfIGghEaLSEA1DeCiCGFMR0E++cJKWt+yBA2IN6Aw6DpweHRFmyRfZqqXBCwzAlPT0c8PGhGzv1h wTxlTU4pASmbloOCRmSi23hE5uzGoBiRzalRqrOqaWqszZBi6YOV6QUBoihSJGNkkpVIs0iN1mAp GntfSn1RyeakWaqzZjCzSBB/b89iCCK1BEderElruKgTahJ3uSVk114YmFBNxQY0DOBxtL+VECvz NSLPWnVSimpdtkNEIODzz5/wp3/7Hstx1jBBQa3ISSZynCo2z80i641w+jcoKEMhiwTRX0FE/VXi 9Q7D9U77VQq4CFA03ZkDOjWDGAAwoJGLmUMENROnP7dqQQJju9nguD/g4cMHlGmumgsQdZKkHlyR UiNH7ISrXuph1bA11alDjcDFcUDcbCykmisgcgdHIqqj2iRUrM0MJ8yEqDGjVlhMzT5+vZZlV+/2 w/6AtCy6Lu0JTb4wkwhQE7ERYAXNWE1USaOxhmFUP4zQnkOEFgHk/XAJrjNTKNOSCuHct0QzH9pe pdY310SwrVk1caqpyx2Dx3FE4IB50oySdWwE1U/HHSS8zsrXHqfmhwom6OXrXtU6dOBCfIHAm9Sb p9vzTq0ep0dv4vDPDcfT+pqGc7o/ZPWpB0LoW7tqCNVLdfmYSdvWeykF89TKuRMUWAY23mC0d00X cH7utK/rp3/VcZFZ+/1o/bm/stR1S3Wzhxhxfa0On7vra2SIFnvL2SLIuP6mFUbrhTrju/XzL3u1 Rl5w6PxLBuL0eG2tvf1rc7gTBkFDJlueCyWOKhVpWGmRCNAWgg3mFPF0zPj0NOPnxxkfHhY8HQhz 2UCwAWhUaUbUR4GtogaDUQohzcA0ZxynGfMiyIkBilqVNGhJdanmWAULqrEoOB5nHPYzjlPCsqj2 oZYpN8biBMwTYgGwMEdLTGVpwSVY6m3zUQURKHCtQMlsER7w7B3coXcdfc+9EFjj/z2agcSlcf3s Zbr1bzeHOJgoBija9Rq8Si1Koy5GqqGuhQjFzB4SNF14GDbgYQCFCB5G0BA1X4hVAvWkYUUKxMpt 6xwJtpExxoDj0zO+/+57PHz8gpINRIhYoimx7InlRXDhf5+lvj0BG27sUX5MWitCdx8yAXy1xebm ChQikIvlYmiOnLo27F6WEC2LoORGiL0KqhPzYpE/IQSM44DD0zMePnyEzEmBoNlHW6ArmjOn7Y6a 4RS9Vd5MUma9q/0qBZHUgXJeZlAM2FxfAYPmcgGkOijXMbS1V8fKJO7s+VxsnJu9H1VLoMxz3Xf1 ibDKsBQwTRMOz5Z8SlRX0YCg00/XTBnY6qgNmfOeEIGHiHHc1OquNZeMv0wXsmYe5m9lbWIzh+TO B4V8jKkzY4hqitTEpeNXSlFNkauJo2p/lmVGmhdI0rUdTFqvEjKv1+bXHN6//vMlQvsS7T5d/wBa rpjGo1f3rH1/iZ1WQOEfxZhYBxhoLZ/LyW/OD29P80rx+dA5Qadd6PaANb411zUxBCkZyzRjnmbk RX0ViFnzmXRhzT4+bwGLZr77ZcfZfU+EiNNTZwJTdxHHgHG7w/XtLW7v7iw3RsE0Tyi5gThmTdQn RtvAjW5+NZDAev2Qr+e/Frh4C8q+fg8FFqq58KqjazSv0ox+Dxqx5Ij9AXh8mvHpacGfHw54nIA5 M7JEM504YcpAWRBZEF1wKQU5ZaQsyIWQC2NeBPOUMS8aGtochiwUVSzcac6YJjWHlKIeuAKgUEGR hCwZ4l2SgiW7Y5cRLmeq3rc6RoCbYAKHiqo1BC5oKnIOFs6p6ZKTKPOJISKaZBwruPAX17wBwd6j WMipSYMK7CrMg1uuiep/Bo4IoABQgFiSMbCWYOfNgDhuETcb9acwDQUFvaYYgs6kjDdLi1KQoqAn MjAyYzuos+ZPP/yIn374M5bjrGMksIVsEUQmiV4CF6t1R+vvV9/V8C+TUmHjYgSqEFC2Iza31wjD oJoL6cBFJ3IU06rk0sJOXUKo8eg260U0O2UIWsBs//iExw8fgWThvk7oTsKk2Z6lYbzaxgb3tP3q syA13M6LQA1gUBHMeVHQtNsi7rYWJ9/tN5eIL4zlqXZDv3QiayaYSnSU+BPTqnYKEyFyQEkF+6cn IKWa2dVGrLEK0lWpfkqaIbMULekeoaa0qWQIE4ZRNRi9NA70Goy+XfX2VStSbdWCmikXaBf7dqjC vfeVA3KxnAOBkSw52GYz6ngfjpAlmVZRBYR6974xLxyvpQ6owOJVMr1mDGtQQZcue6klvqDPbk/9 +6p95+047+6lAWjaDwfR1am//0cNQDYtA7VbqjrK1p3uqZwyFosoORwOyFQQBp071369NSA9+Hpz +C8cp/N4alXpP1JvUnENXp9vpQgkqIPz9c0N7r95j3fv7hBDxDRNmOel3tNNZa4579vzIqjoaOPq c/c36K8FLn7xPah7J4vb11dLoNVQEQXVGIAGCG2wPwIfPx3w8dMRX44Fz5kxYwAQzTlPrIDWDJKE gQWRVUrJOWlp52T2bWIE3kAKI2fCsmTMs05IEWhZb2L1rZgWzHNCSgo2mAM4EEAZwQqmAurk56zF E6IoANGIE137xrCFKiH3TJh9xEoR/R1xAEKEGLjIbBoPZgUQZOpZaSClv49mrtOKkoEUtARXlVjq 6ZaaGLZgPBGZaiWIQgUL4AiOUU0cw4Cw2WLYbBHHjRVds4RgRDX7qH92R9BcMkouGDmARTAGwsCE SITD0yO+/+N3ePryABRbyAUAOwGROtfUgYdTpK/nHFW2ixpjtOghIrNBAqGg2v8LE1II2L27QRwG lVpFw4UFBhI9g2pR34mUOnW8reE+7EvgCcaUSMQYsH94wOPHT1Ur4k5YLUpWquaCQC1WHw1Y+DrT dNqomgAyyB6MggkBc8nITNhdXyFyNAzJVavSazBU7e/3l+pbUwEAd6BCP1atmzPoOAzGfEz7ZsTx eDggHQ/oS5w2Z7tGJ1ZmRRhwtrwfS0lIpu3ZbEY1w5ipAgCy+Si1e1InnTbmoEter8oltQgfFZEt 6Rc6zUGTvIMlbVJfm4IQAiJHhMBYpgl5WazsvJsIOuBCvebplaNDTCtgh3N+vz7WAPGMsXXv5IPS t2hNrlfnq/as7qm2r5x+X3rYC/qPNcygHli4kLkGF34Xgay0ZXozqaCnmqI6c0xJGdNxxjQdkXJW /6AQOy3j6cA0Wi4nw/OXcMleC/HS/VpuEh+czn8LqBoz1UTob4bNiLv7e7y7e4cYNYQ+5WTJt6T6 WqwXvr/byQsgoo/igpm/yd/pos+FdH+dT/l6IdILr7UaqRF2t+mcXu/MQFX7bLVBNFxSmSmINPOk /V0EkLDBXAY8PgMfv8w4TIy5ROSwQbZ4TA3jzJpRkrTkd7QMiMuslUQ9ERLBMr0hghEqElyWjOOc cJwzjnPGsgimJWNJYrXAyHkvmAQBGTE0NFmJtC8cEHry5mq8Zqd3my/7+tHfVw0HN2JpXuqwOhge v+0ZMIkJQlZrhYzIGjghM7+IJS6ioPVOSjCA4QmNYlQgE6IVvBqsoNkACoM6Zwa/JkCGARQHcDRQ gZbB0ASHShgFqAWxXALeMoMlYxMCRg5IhyM+fP8jfv7zzyhLriYhAayftuYqQZPKbNs+8S3ZGBVJ U2/3a7xqCYlqCmpdj4xEhBIY2+srbLcbEAElpfr8UtRclrJgydlqyviMKsMNRDUlsXQEMJeCGGiz Cg4AACAASURBVCM2gfHw4QP2jw9gQWdHP1F/dzuxNBpuxKdjxCeEfkX8mCBMWLImPhtCxG5r/iSG DAo0XwCKlnIPBi7cSdSLY1enX1tjp1zI58xfrpGDiGXxDDhOR8z7PcqyNNphfk6QtbNunVcCyGuw WHbCsiSUlFUrEgN4GDQ5mdes6PPL9ODChRHpNKSElo9GGtipo0mNrmULxdSoM16FHAMaPghSHxHN NKptZIFVU9X7lW6c6pRTP5JdCLKNU5/xc+UdcYHT1VV/AYW0/dLxmdVLVhf3nz3Cw/k3rI0rriEd b/EOijsxSje2bRCUmVJ9r4QRqPOy7qsTGH0Xd3qHAF5ssD6rY9ZFsMwLlsOEdEyQVMDEGC2qpIJI rvFDOEUddf+tEMJ65C6Bqbae1uOO03cTfNp9jcYUr6pMQC3CCYDNVLLb4N39HW7v34GHgAzBvCxK X/x+dd27cKX3uwREyde98R/Ngm1CDr+kuaiqV3+/sDpfAArSTfrXA4v2WQFE0A1dwYUWDbOEwOq0 KAwadjguAQ/PhOcDAbSD8AYFrEwoZXDOiAJsImuSnqJJe46HI6Z5BgRmJ9X2em5KJ2ICQhZCKoRp KTgcEw7HBdNSUIqqQGtoj6gEGqBAholVmifPAEpWl8SjCnSTaflmf5bUUuwepwGQpcf2xVQD1yoj bCorVFAADnXiySa/ahDsvZj2QJghHCCBzV/CtBMGIhC80FoADFBotVQFIX4/CYwSggEq74Ostx1Z zoaiERGayVBrrA6BMYhgEwgjR0hK+PzjB/z43Q84Pu01HblrFvr92+9xW7uuwWiJYdzc0y50Qgo0 qQuEmreCXFXA6keSzPyyu77C1fUVAEHOyWiCelkvRTAvSbVhHTBwIjYYANSkapYYy5wSxyFiKILP P/4Z0/6AKgUClVoTWsSDShAeEunb14lvPzjW32a3sXk3plQKgnqdamn5GCCRtRwrW32SJO7dowyQ CJkJBcXCqF1i7fd0Yy29RJZzxhAHxBj12ZZALaUF82GP+XjQpUyk4alEVW1T3RKkSaxtHs2PKAty WrCkBATGsBnBo0XEiIKFkgu6W9m9NZy01OZ3mibYHi8KcoRbDJWrlD18VUFkr50iU+EQwrgBmHCc J6S0aPbbIgilIBqwyKYdacqJDhzX/Do+zNRpp+xpK/7fwNCKYXUMw9dXv4WaDf0ywFiBgI5tViZ5 wkr9GTV8uVshFQi4+bn2v91PGTb6jtbbr9iVrwnq7u3ZnOFU083cjYi4uZALAwuwHGZMh6MlQQNi YMQhmBZOVsGMRDjTONf2ovlwNW1LAxD67G6+aA0uVtyX1qO9+or6MW0D4vmOJBCG3QY39+9w/809 rm5uMWw28EiqnDKCgXxfV2uhjFoJDPD/ou1N0xzHkW3BYwBISvIxIrPusJ23j/e/u1fRq+3Xdasy wsNdA0kM1j/MDAAleURm3dfML9LdJQ4gYDA7NlelVuSfZaSpMuPu9hah+qZN26HtGcrYr4+f+QLt 3ttwDBWS1H53FshJEpkvfuLQghYh8rIQgd2EJQX8+ChY1gByI+AGsQQUyXIYiDA6D09AWhMupwvO pwtSzFqvhyANyQK8C3AUYB1EhSBIHkjNshCzCI81iWYqsQ8W4Fl0Dh3YBUiBL1mU4D3GIJqRAysi dMiFkSyATYFE3T/cip03bK/M0Gh3M15bi27bXsFkNi1YhYC5J9jQdWfhsJ+s81CIQBRgpbrbdUaM ThhjNY+5jnw040JrDuRSak8VAJob7TAQY+c9EDO+//MP/PN//b84fxyB1HpaiKZmaWkt+rxqsgZo KwVTnSOyObB5rebWfp64aUowEOaQCGDv8Pj8jGG/qzEVAIELI+WCJWUkq3RJTVsxUOQMaJBW3dR1 ylwwOIdymaX097zUAVXmSS1rY3Pv7rPrfAOufL7me1Qg2tMZrP8LOYyHPYbdhAyuQYk5Sdl6p3vW KsBWgaKan/W8sY/sjD74UtbbacAlqs/XASjriuV8kaJoGrRraYXiuqT2Xt07NhAmM8YoWHNEYUYY RwzDqPUBOnePxvq0deHG8my+1cpkAqCUGg6qgqWzfNhcQi1fzjRAqsLFey+F5rhgnReUuGrOmdBQ 21MdP+3WtBfw7Ysmqfrz7h33gvE2h1kLPrlBb76XR9+TD7fPaJlFbUduntnfA1fvb/fjq1Or8YI3 H21ojboTN5/pOOrv+mSlBXBBShGX+YJ5nlG4IAwDQhgqAuh3W1WeuJt7QkMOPZqoT++GczO+f/24 ldrtC+ccdrs9nl9f8PL6it1+J/K2lNbVWcFeBUwkFldH4lIlc73DeJLT2CmnQcoM/3/8z//xfwPd 4l0thND4LfH0xzZq+D7BtrWnhnw6q4WlwNjnEgRZIDqClSfWokokrpJEA+bV4/vbilQkXZSc1wwJ YDeO2A8DUlpx/vjAfLkgpyLaIjzAHiUzSgbAHhISppoLspryVXCocDVTaSmMmAtikn9JU04LeRAF JIYEWKpJ13kHCcMoGIODd1YSOSAWYM2MxCK8zTUieNt+GoNuJtCesTTW1lBmY7Kon7W/AWtKdn0t 19TFHvyZJKHuecaDm4XKglXNnA1YASnpw2EVEy3DwDT4oJ39PICJCLxGfHx/w9//n/+F92/fgSyC t2fkVulmq4FR3cNOwYHldovmLGLBMifMnWCJNr2wbJU6tZqoc8iOQOOAxy8vCNOEmHMFcFGtFUkb gd0LtvPOiSenMzVWfYql3Prl/R3HHz+QY8KGfXGzwhjA2uwxXeU+XFXWr2Nh+rsj0z0B01BYA2tj LpgeDhj2uw5gE4q2l5f5bfEc9e6WakvuLg+4p285p6mpen7wDq4UzMcTVu14a1ZEW9/NXWwvoI2j xvIQwFxQksSMeOcx7XbiilGLjbW0l8qMVIUqd1LMK8AwQJgti4aaIO1jUoAm7FzV7LosGqj1CoS8 rkjLAs6ahmZKA0HL8KPihmtwcYs2OhC61eJ+DSi2Z1/9vH+fzRk392xrdv/+PxtDty+uTzM3h55A 9lF31MDwO8Cj3pANwNAV6GnXmFzLJWNZFyzLqrF3rDFq2iVbr7H+TZtX1lelO1O6mYXrc/674EJf 595KGs06H7DbT3h+ecGX37/i8fUZYTeKfC3WasAsSWbTb2DDUZ9JKFI6KAjxAPz/eQUu6PoN7xQD uaYX6gTdFgXK0dZWhiEBKFfmFRBE4OsrEKQojqJDB9SALalBEJDKiPPq8fYRUTCBIJHhIQC74PD6 9IDnww4lzjh/vCMtC0zzkjQcD85ASYxSCNJwqWj6p2izBdYuXYU7OSnjbBkaCVhjxrIWLIkRCwEh IJEHSNNXmbU6plTQDF4IMAwD4DyWlHFeE9ZMSCBrkVKj/xuosDkmXRqqWSalm/JKAnpOE2I9MCA0 43+33pAy1Tdrrvd05Fp+dbcjaqQwIFYLlbhJLRTW44HBtcCOABB7Q8LgHEbvgXXB8ccb/vn3v+P9 +xvKGhHs/Vn8pzdaADUrhaX3mbAAGi7qQQaoxcEAajqss0Ko/nZoVotz4OCwe3rC4+sL3BCQcgYU PMVllb/12g1DBzaBYX2+eWWGkPiN0/dvWI4noHDzx+tG8hpT0O9XKydtn236A+gz+oGUOv/CRJ1a 1lhPTbloS/Mdwm7U1Gm5hs3apFK4mv2NGV9z+vpmhlEakGFmzY4ZQJqW6snDA5iPJ1xO5/oWxCzF 4NBY0jYkswkkUt84ESF4Dy4sJbgLY5pGqQLrHeCdRte3VtUwYE823maFAaH2gbE6IJt3R3u/qvUR aSVh6ixIEoQXvBdLTSo1qJxcc3n2JSYayL/lvz24Ms35+vg1oOiBUbefb3g9bd61ft4PTN1yZgHe GBFu5MzdkWzOr7KW759jf3fQoM5zVSxUc2BFLPXxCuiu789ac8BSpnMpNaNkniWlM/iAaRgFPFrN AQUpwqt7a8otv4LyKQP37e8ry8v1m/5qLblzWdF2f3SvLJZz7xCmAfunBzz/9oovf/sNr1++4PB4 wLSbUMDa5qAV9DMrZFWW7J+tFUn5+3B3sDeWimuz052uk59e38ztlUxUem69dj2Q0Xtu0KQxDzVh O4ecgDUVMAIAj8IOjhPACx4eRvz2csBhGvAwArvA+PbHG46nC9aUK3J1zoMLoaSMNS9wEfB7kgQH P4BcWwyGaOM5Z3F1IAAkTCamgjknuDVjLoRpDNiPHqOT+hG+FAwE7REhPTCcE0ZgVpCVoaWszcxv FoHtDCvrqVPN3bKbqLY/WampuVR0kykVmNZ9s+Do9poyGyuLDZQ6JqbegMlqXW9ppblmygBFS5OT 10px2inS0C4xgBTx49s3fPzxT7y/vYFylkqpmZFj1j4e3bh7UjO0fkW/XH8215HNm71vs8Rw+z9Z HRJprAbv4MYRjy8vGKaduEoAMBNSKkhZ4mV6q8SmTgSpZWfLbzfruq4rlstFOmna3MOYetkwS2P0 9W3VDXO1jJuZuGVLrAWiLFSY4HLB+e0H9k9PGB/3oCDr7jUluGTU+id1fPaOzqrX9qmphvC2T64V Se1dNIDZDwOmwx5uDMjLCrPCle6ZIqirmcGULFh9BiMNzwQqBTEmLD/e8RY8voSA8PSA4D3CMFRL GthiJPQBWmq9cAEYtXz5MIQKKGrn1boEbV1yzljXFXCEQFBftqxjYUIYRjy+fgGyCK5TVN8+a2G2 Tjh2OPm+Rl+/MPhsNPBzQdQDhevKnxVs0e35m2uvhrBxC3Abe3uGCG/SolaV39R7q2uZm5IkfyoQ cE7P6Wx0paDAgYw3kVxfSAR+n8Ugz5LnO+dB9b6941npOaurnqVSaOKEUzoizQlpSUhfIw7PBwya lcRQfmeT0DGpu7Cb2jzb33/m6NepFSDr10N/ZzS3dV0Si0nRcvUEkHcYxj2eDxKT8bf//Df8+P6G 7//4A2/fvuP44wOXjxPW8wU5ZnDRooDcCt6JO6ml1/v/60+mom4CBn9JvJXdgLuHip9GCmMxaxlo MaDIP3IatKlpd0okVEM5AVKLAvsRp4Xw45gwxwGFBxAcHGWMPuL5MODxMGIKwH70eH6csN+NcI51 c0t+fO13AAAlo5SMlFcpAWuCmghEHhbAYmWd6/s5CXQs7JC4YE4Jc0qIKSFljUxWO5UEczJAcs2S GO+XFZeYkFgCYlJJou2rmTqzmJtz0ZobZEF83FITq7AXeu6jzZt2rgWiyNbm1nLB3c/NP72/rUnX VQTiRmIFMVowzJiviXSZRlR/pnZ7HZ3HfhjhmTB/fOD47Ru+//3vOL9/oKwJo/MI5EBZQlq964ON mgYgtKiA8Yoa24Y1YddcJwSN9un+rr+RU3BBwBAwPBywf37C89ev1WqxJikbn1KEGFTa0xkAa7VJ 71oqZAVmZoHpmMN8OuLy9gbEJFkZFg8gUq5mQ8gdqL2SveGVZtgEfLdPiRpj4e2YzY+aUgINHuN+ Dxo84DUouAiA3Dg12zAEVBrIu5r2NjFb/tFiL0jXg5FjxPl0xrquzbqlQGJT4Im6dzS2yvKdU5qU lFGx8i3rCj8M0lMiSLqogDXdJQYu+rlTJFsjVmoMFm6sFt0Uy76xOJH6jjZrMklSk8YrEJF+EFYd t66zkfgvgEKj36vPf3LddXzE59d2jOZnw6CmI/frf5cOdd/17qLtO/Rwaiuat8K1O1/pr1+X/lZb 8dX4VFXEKr9S2aDISLKF1EnHQF4zLqeLutolEDJo7RZb+/559Kt5u56zmxlp/0xNb69C6BXzGzq4 4REA1WBk1AwPK7RHwWHY7bB/fsLr337D7//x7/j9P/4Nr79/xdOXVzy+PmP3+IDxsAMGh+IJ7Akc PEpw9d9f7i1CNrp7B6MK44oqGRDHrJmJtXspEcQzIy6G6isvAGpPBWV+aGiIIRORWbI3TpeIgp3g WEWewTEGzxicdvJEweEQMI2vOBxGPL4d8Y9/vuH79zNSFksBzNwJIKYkGScZoJCl0Zi3thhNOFtF 4KwrJG4UMaGtMWONGWdH2HnGfvA4jA6jJwQ3wPuAQg5zFmARS0FxUna6try+VnFVi/Cdv5dJU+nI mL2rBNc2SyNF06zlrx7jtsPcIjLXdh2DteZ+BS9cqg3LhMRm2FWIdwGTyjQHknQ9z0A+z5iPJ7x9 +wMfP96Qz2egZAQTJpxVozSAKfUmuu5C6CPQTVe40RQIbVv2zMa+1s8KNbSficDeIewm7J+f8fT1 C8JuhyVFrDEhpoxU0x4dvDZm64W9g4Hrbr5JwKLv9xIz0ipdG4O9g2k+ZDJOBaaCEgJVy5B9BiJs LY3dO9LWLWHrZqlsUtIdSCljfv/A8e0Nh9HD+R3gAD8GXfwMZDlf4n7FxQBn48IGNG2OThjnnLEs C5xzGMdRCmKRw3R4wLg/4Hw8A9rfB2AJKO4W75oTEagGCMNoxgKXc0FeIn784w/kUvDMjOnpAbth lM6ZvFZNTCyKpQG7K57nvccQAoqmUG9fT7RCa0iXYhRLpwsIgyWQEpK2rA/7PZ5++w3LmvAjZeRZ 19+IuCq/Op9XL01dbFEdw4as7unMn4MOOf3+NbfIsXtnoO7PGzDA5sLtPy92xc039dU5N6ufKndy ael4sdI1Ky0rEKsAm/UyZrATpczGKcHg8pmr0QNFZI5qZlKgT5qbITcgklLER/rAsiw4Ho94+fqC l68v2D3s1DprylmzItg+vplV6pe4hw+frMKdmzQA07oVd1fUeW03sPEpuGbhe+Qc2AMIBD8FDA97 HL484+t//hvyGhHnFfN5xnK54HK+YJlnxGVFjBExSk+ewvyvNC6jjtihE900KYIDcx+oKUCj1Swn ARrwAMLGqgFyYllAlDVUQUiQFFKGEJr1rVhywRzzJvgElCFdE62WgErCksGlIHiHh/2Ey8MB65pw OieUkpSIpJhUZpIK4SXDmoBSyChhkBQ9iPnXGKmVFQbM2iIWgQRGTAlzzDitCbvosQse+2nEpLUm 5kJIEJotpWhKp2u7iwAUbuTC1liHKtgQcGEaOEMyYJppv2IVpamawsaWAmukKPdsMQBNCygV5bd7 FdUQZQ19BdAMzTBw0syM9MEiZAmuMCbv4JmwnM/4+Oc3vL99x3I8IadV4lOgiaNcpN28akSsJs7K vkzIGiPB526H/sONdUPfw4OklgVQC4llItA4YHx6wuHlGcN+L3UhUsK8rNLOO1uJtAYM+4qWfW0G 20KWASQkxBLIyA7rvGjGBbr34uoHt/v2W/BnWhEzV19/b5beTIYK1Fr0qgADOazzitPbO8LDAftx hDXuc96huAxO6i7gLsiSSFKf7wCLTaEqolpePMaIoHUEPDlkEMK0w7jfww0BZYkAigiPzuxtYKvX 4khHKUqJkgeZZU0E0Ho84aMUBOcxhgHjfkIJA0opWGIUS0c/PRbrwQ3sOecQhgE551qj5d5hcSp5 TUhuFXoI4qPmIm5DFzympyc8LhHrsmJNEblkCUgmex/lMTaWKxqA7WulL4vDuR7LNW18Hnjb6O72 nfp72N8GaO3aJvTljlzn8Br8XlPz9l9PqWLtbDVsDPRRpbtSivIHpT9VxIryDXlnVoug9htxxk6F Tkj5jrg4JBDYO0gdIKVbEMMFASDLecayzJjPZ8znM15//4LHl2cMY2jWNnu1/r2vgEbvYP7ZcQ0W N2uoD+IOaVL3f4A3dGJTWCoIUosztTVzRrPBIYwDxscHPIEEWGcpfJiztHoXF6NY68M9k96vXkJ4 cf9Cxr0smr/Lc+cWuAkFHswEWNpnH9RJDo4YHBzIGn0xAdZjhAmFJIMkMWFNkp2YckEIADgjoCD4 gCFMcAjIKaLEjBhXXC4zLkvEEhOcc3h8eIT3rdJmjgU5a68R2KYRkrO6DI6Dlvi2AlntHLNkMiDp nCAUEpdP5IJ5YYQ1YVwJhx0heMYcGRFaJwIAFBxUYFB/Nr28MjlmaaCmnNTKPDcYbESlwIfFFO6Y NgGgrab/1qHQzIVdrAKwMaPX2A0bIykgIAFn1mQtaOVQ0n4l+TLjfLrg+PaG47c3rOcTKDNGZwGO XN2Wkjeub1OtLg389BoK0HD7lma3yJ3qWHULmcZPguYLEeA9/DRg9/SMxy+vGPZ7JGYgicUixihZ CGhgzrRdE3wGqOqgSWJs0KVEkncYhxExzVguc2MD1UIh72pWjpbRQlXg9O96/+gYeJchIX+aYGyu Eu8dKDOW4wnLxwm7xwd4P4m2LYuitMpgOLuliSR1j/CGmW7GR1QXzOITcs5wQSwgPnhMhwOG3Q7L GisAZHCNqbE7c/cbiDRLqHnPe/olhsRgHM84he/YTzvsndQvGIYBMWdYJ2C7bQ2YReN7lstvACNq MaK+H4XRAgCwngPnMFCAc1I9uGRGKowQAnZPT3haV7zPZ5QkClZ1XLL+z+jV2KtN569l0oafX++L 66Mt1e33t5c0HmNLfr3UQq+tyrABoO6u3b10ptXiZECbayyGgjblv1VxNUBlymadI7V+mGJCGmBP vmY7iTB2jb5qISqo5gcpIueD8gkGJwEo5GStLx9npBilxPay4vn1BbudltN3VJU0A2MSGN/J3zvY 4ldWpxv3D1B5sd2rbldUcVX3aXUhdrRUCFcdtGUvZUBiEG0lyIGCq4ENmzHgs4DO21e5+nnvd96+ gpbwliGqHkoepCW5ibxUibRqnGx+YJkYHwJSTkhRcusHL31GSlmBIWBdgHllxOyR2YFzhuMVngow BnDxWOeCZZmxnE6SSrRGxFQA8shECH7Cw2GPccxYlhXzecW8RqC0uAGwBlWRtFjOOUlhLCcaHKkv 2mlDNdHmg2hlpODC4CEXRC64LIxLifCIKCUhZrFYQJlGLc7DqEi6gFXLFaLvzW22hhqkXQUEoNez Vcdk3eLcQFB3biOMq9UnVIHpXNMoqN5E5klFJTwJqgWL33IIQXzea0RcLojnGfPpjOP3H1iPH+CY EVhbJ+dcSas+iVA3pI2vovGeDPn6F+rOVUBx83I274zCUmMkkVgwpsMBD68vOLw8I+x2KGr6j0sU QcEGUlzT2qgPDt1yWIIFiBahf5IS4YdxwhACjt9nlJRajxfo3F5pcE0Hke9qa/LOMrA585rb99NE /fcqvPR3x4x1XjC/f4gmtpskhdU7LZZmgXVoGo4WBqvgt7cqddanBmilym1KGTkXeF8q7Y/7Pab9 HsvxtJlTRkPxrMJl8wwWmmcTApUP6TiZwDljfj/iffwDcITp9QnjEBBDQOZUaZs370CV5EoR65r3 HuM0AUAFGNB39Jo1ZdyxpIQVEGuEoxrgWQojE+DHEY8vryjnIz7WGelykYuzWGO9l5iufj1lCbv1 /RMgw8b3U75fhcuvZAPu0te1xUr4R+n2BOnIr3gYm8IAMKnrg412qLN8UC1BbzQIbTpW2Nwo7Z98 37nLSF2rbNdJ4K9Z2aUcQqnAo0ABXO54rZZGIE9Kx8Lz3vMPrPOCy/GM55cXPD0/YTrsTDLLnJDw T08q2cHgIrKuapX9nNyd9jvAD8YXWJ/RvrPS4du/jYX3rmtZH9NRe5Wzsnt0POre2Kj8WbfIPXBx HTYnn3FtOlYN26AuaJO0WJWWkVJG1i0ogAJp/LOujHlOEuK5HyR91Emr9SVmLCuQOYDhEXMG5QWO GDkFnI4rzuWCy/mIuMw6MQTGAGbphWqAZwwDPA0Y/IQxJpzOM5CyNEViBudUTXFWhdNSG31w0kcg eC2LrG6NYhWGCYVCXSyA4TxjLhlUkm5iL6Y5AwFEdQENRDDUZ9kJ2cq50PEUMgbcE5oKvLpBuy3d ManrozHKbuVFNajmfsumIWbAghd15b0GN7lUsK4XnD9OWE4nHN9+IC0L0ryAUsRAXmpYqPvHU2Oa Um7ZALC+DVX6l1ewjWjmPKpiF+h+swp8Nk9FmVUtX03SIj67gP3LM56//ob9wwEYg1SjVG10nhfk mLVyZF+yajuLVMeN5oqCCEPTZEDAOE1whXE+nTduD9eNVWa9d/9gOxcbYbG9sGGILsOkCiMFE8z1 XoVt3gk+Z8TjCevHB3YPB7hRC6gFD1YQoZZnEEkd3awM3VKWmwXmSsPSf0WLj5k5lUkyNIbdhGm/ bxYWITSwFR6zAXNbP+r+rvtHhR9V64oUrMpxxce3bygEfB0CxtdHTCFgyVJg73o9m/1CmbHS+KAN 7EopEghre6pooTxo+XvNjEkKLIKeZ4LLhYDp4YDn33/HcjlhiRrMygBncQ86Z/EybQ+0d7W3+28e HbC4C0K42VFlHXBFf/2p/WgMhJDGYNwf6TV4bp/a3CugLyqvWV0dZFWKCKyA8/qf0/cSpSJrWQOx VguAEeWz0owuQNH4rkJZa1uIBZ4ZoNLFAXFBKitOa8RyXnD+OCP+vuL1ty+Y9jsp/w6p5CpF1uQy KeZWlEdvy+DdAxifWjTaEt3oXdyd0X9HHc+Rv1Ez8mS5aHOf6+dvVqkqV4zwa6vF7dA38ahGhyBY 4CazZIXUDAuyDBD5TDR7KzClhUjI1WJLS2RcloSPjxmXywxPDvEReDqM8JMU9kk5IRUGw4NpQM4L OEV4KphXj4GBUhLWyyqb3CtBgKRkNXsRLDVJXJgEeQECQ8pYomioSbVwMSOhpo8yGDkTvC/wRWvP Ow9yWdhXJRTzaRmUVEFs3UddYzLF3CuVWTSMaMtZ9bAO4fYrvWE2PZAwKqBe7GILb5VA+vvYxdXY zBZPIDexmAhp0y4FZrgUlBQxrzPiecbldMTldEaeFyzzDFcKAsSMbBXdBMTLHNlGcLyhbVjMTf/i 9p72KlY5shYS0jXbvKaWLM+s1VGdMHceAh6eXnD4+orp+UlqlmgvgpQTokb0G5DSh3cbger8gLBx i9RTfQMWwxAwjgOW4xmn4wmaOdet2xYgyateTUqdiPaC8nXR66hyG7uUO5DYUIp8KVqZ5cTs/wAA IABJREFUad4OeY6Yfxyxf37BODxKITEnVjvOUkCK9B7CxI3O2gJtYkWUWYr3QT61tFQLOC4FGEPA MI0gLzVlBMiwZYhuwJIdhZp4be4urjRg62Q9TZZ5QX57Q9hNeB2kqBDCiKWsyOrW6ANgG0EpMIJY 58IwbDrE2jqxWu+s2RkzI2tfI5CMw+ZFgtKljsrDly84ny/Il4vyR6k1EDQeq8Vg2fwqP+1p8RfH NXC4SS39iXVjK9xIaUuv1xdqEFcUpO2zVJBaWnx3T673arR5HUFiVhAubmO0afF3eiOiGp9h61hr zaC5Surn3Gi4OBKEzxlEDo6LWKxJek9xAThL5iPVfaaWMQfEvOIjvqPEjPky4+nlCU9fpAW6V6ta zhLXQRojyLYuuD/vf+agjVu824bU/dXzCpPr3M7vSb23Ixh19WpMB4MayGD8lYDOnltshVodHCRO QmIo1K+opaShQAJwyIXUR6t5xs5LrYco7cs/TiuO5wWn4wXrmgVcLEBcMnaPI/xhwhozYnSIyYEh LdOREkoQUMFuQHAOPA5Imaqrw4phmWkMUIJkrgJut58wFCDEKHUHokTCWvAKm7ancSK5MPKSAEoo JJXP4KV5F5yHWDuab1+qApaa3WGzKIGc0m+DFeXzdvXaeOua6G89E1DilB4s/RJuAzKvV9eCgNgu qiCikZJT4EMMeG6iT6UvCnKNHl7OZ1wuZ8R50SqEGVQyRsjmrWFXmnXiapGkrUC0QdvGawXG2vv2 P69gx1bLAjSrR+YmEZC9g5tGjA8P8Ic9Hl9/A41B2sI70XNjSliWFXFZpXdFf1eLMUGVlbpbqP4E GdtvVWK8D9jt9nDO43ySuRLTbGOlvew34XRDEN0a2ls23sG672w2JKYHSusiYHwDFsyaOcVwRcy2 JWbEjxOW4wnT0wOsR4Z0xM1ivjaaqwPh7t/teLnSJumYLNVasrNYUeUwThimEesySwGvDVNEB8KE C9a9CQEZZq2iDnRU2EwE5whpXfH+7RuC9/jtP/5dgleZMd8J0iRqZnqwtm+HCKTacTKlGnMicReu AnqAUHJBWqUxm5uEX9i7ZDB8CDi8fsXjZcb7f/0D6TIjOA9nlUEtALGb2j7b4M/Di/sAbePKwk/A bLvA1I7GELhb9Q1r4go+G9inKxLRusjsuuubFYXrG1rgsH7nGrVJvQvX5kOvtfeSUhlUaX4DMACx hIA0tkB+KxCF0JXOrcmSNFA7TmucoINaJyPj9OOIy2XG8XjCZV7w8vUFh4cD/BjU1a0ZSlpI7t6W +Zl75N7hruKS7HdzedpyNh7WQ4R2ZU9TMg6718bEsLmZkcC/kC1y524mIHVyyUkpbrNIyOJqOhhM axO3RGGgxIx1WXG5LDjNCT8+VswrIycC0Q6lAKdTQoonjPOC6Tnj41xwmYFlLWDOKCXBa9pSGIFh hJY4dkB0yEnBA0iqjZGMu/pvod87ApyHB2HygPMOfvBYFg9aVywpSnMicqCglgll+qkwspRcAkKG HxjkWYSUPQu6qVQ7NxeIlfcWc1sroNSJ727ab9HnTbCWMYjeHE2tkFyPPOt13DEpoAUcAt14GMjq rGExoeeUsK4r1kW6zK7nC9K6SlXEXMRepL1eSDU9AyhE0hHTXBlFNT1hTlS1ziYW2LISG8Hr+zlU sdEBXmwsFgyxImWW3jXZO7j9HvunRzx8ecH0+AgXRiR9DjlCyox5jVi1gZGN7Z4B2mINbI02Pt92 kpjAJzH7pyXi4/0dKSb4Ll7h2ixdfaif8Pk6H3fG9JnJuneTmBARUzGkoRg7uAKkecV8POGwrGA3 AZ6kymU1I3Q0eEWLMmS6ebZUam0l3XPOiCnDjWJpRGEM44Tdbo94/ABKDzoF+NYaLu1/yEqwnhXE MlfLigHAzEXSWr2HI2A+nfHhvuOwP2D39QWDD8hDqUG7+jIdQLudaafVRgszSsp1PGa1kSvk95wS 4EhaAPjQrCMkgXPhYY+X335Hnhec1wRwhHMBJa81xmYjvOv7/wQEfHLcAIvr75SH3BNtm8DQHgSg 0X9vmbhxy8kXCqDtXvIubJY1BQHoaLQKwM4Feus6MIDRACewLWxnP62ejPDNJnzBaP1hUAB2cMXB U1GrOyS+jCwerfEA0qBQqVG04iNlzPOM4/EDr79/wevXV+wOe5ALyJw7nn1/nX4VhAt0Co1tRftX QYGsU3+L2lOnLQcqxBDU08+qPUgBYH0w2qpIzNOfABfU/WsPrfCTr84jB6fgwsxE1aRYTZQNZMQU sVxWnM8zzucZl5WxJI9UHMAB5AYAUnhqWQrWEvERjzingLgGlCwLKIVyHHwAxtHBeWl65AbG6D1y EsuIlGpgBSRlm8IJCciUCs4iAJ0nTH6UojvDAKdCRopcQTorOgFKjsQ0WYqk5eTMIJ+1tXmoKZQg c6sA8OK7S+qD9AT0lUnv0RnDw8Rk0S6NoglKoEfQzecMXCjBSiofUEP70YJAe9phnRdFhRI5nNXk mwuKVjRMa5Qo/5gkx3ldkVMC5QJOCVSkFLcnyFoQw5FHyVmHpeOiNo7qekLTSC1WpZo5q8+9Yo/K XyVeoOrQep78binM7DwQPMJuwu5hj+HxEdPjAcNhDxpHxMywxmsxZSxaz6ICmh7Y2NObjG07x4AF NY1BmKjUdBjHEcF5HM/vOB4/BITVinf3Dza8CIto2izdz48Kehp9mb+6vg1z1czlVCm7nXLGerzg /H7EbghildNOpkwZRK0UNq7M0J0oaENRmjVhWxRcZKWtYRikpssQMOymJliMSmrGCwB23fwKUTT6 hfqQZcYsdqameysQpVywHs/48Y8/gCHAPewx+ICSMmKOtYR3KVIDwZqouW4VCMAwDNK0LuPKnGxE 0+a1aDfg4nOt30DkUHSf7J6f8DL/jnyeET/eAZKqwIwGXHo8IbT1Syr4CXlwfY+rLxr4QRNydk0V enbu5uJbUNnfV3jQtdDsf3YKnIkYu9QxQGJN2NCWWSkgluANELNzSkGtLqzgwmmBxAYuzH2sAtuR +q7UhWuWMnVlF+NFheC8DLIof3MsVaDn0wUxrpJosMx4/foF+4c9wihWfXBziH92fGbFaIGraGii W4LWoHH7XcuM7OanyvlOMbiiLe6u629rsVGhndY/z54kGrdKANRURUUzlWWrJUI+9vBuBxSdKGbU aF47ckHOwLJkHE8LTscZ85wQE5DhUJwU2srskHPW1KAAhpoULwUJhADC3jMKRxSIPzKQ+DeZrd6F +DFZigwiRpafmUU71bRPqathQWilm61m8gR5OD9iHJMUUFpXaVxVCVXK/MJ5SMOkJO4aIrgwyj1c KwRTCJrf05bAtG6LtOC60Fx9aUbwFrGcof07clEQkAUElKx9JLRGft0krQ6BracBQGYNrgNLgCVr ieakvUGKVE+UNulJouY7AGk1AsQi4eGNTE0wkDVyss0gIM2ktgQ7sswpCEQZzE6FV1fhUoFPH3QK iK/TGrKBNOsHEPdGCID3CLs9hsMe0+Mjhoc9aCeBVtlJASyn5tSSM1KM0nK59pHIECtUM1/22oYF n1ZB6KjGdhQA7B2GaUIYBuymCUgFl9MJ8+kIylnqONzTTDrAaXNgn9wwzpvPuNNA++upu6LBFSrC SItmLXERZlzmBcvbO/bPj+DByfeeAAdpaqZA1tqWN0VEn6L1O8RlhxqtbjQNlqBHJAZ5SYejIcBN I4rroBTLPTYuKBbmCTJ5UjkdLBi6iUCuQLsUDfRjADHi9P0NHDye//137B4P4BAQc0RB6WpfqBBm ad7UWx2IXG0jn5IEf9cCydSvGwHMiGsEgTCMgzZwEypKXDCEAQ9fvmA9nfB9uSDGKN1Ui4IrrYFh bkzarOe/drSAvO3RB4Hfi9Ug2gpwozcx+SvA7mmvn0vePrPC3wpaTM24ejUuQJGsDrlOs3OoAyqd gmXpq1Kd0lULULtdUaJ0qnTWLkP12UQFzFowjkROMKSTsBTnUosGSbA3gcDZrmUFqBnnfEReEi7v J7x8ecXr1y84PD4oW7P6+iphu3cx2XDNIuhqbpqlAtWN3MO+jVsZdyiGurNv0Ob9j7cWMHOLXI1M AEoreiWgQsQE24M7IVfRPzmpXQEPcYNoi2ZrdqUtjlNinM8z3t9nfBxXLDOjZIcCDyYPDip4wdoO WVO2OFQmDRAGx/DEAjhIYjMG8gjkQVr9jgqDkaWg1hgwBsKaCtyaQcRIBciZJLCGLfjHFo+VKFQY wmmZ1wHBJ0QfsMRVg0sLwBkF0h0OIGkZn7PEeqSM4oOYYYcBIAd4r93ctVMmM/oaBH1gWF0hZayy KRzgGZ68pm1lgBJ4ZXDJiDFhXTXWgWWeULIG6mk6i61dBRfSS0OEilpEcq4FjwxAGNlIBzyqlilm LbVUacpM0nKF+TUbEfaan3IdLwGWpgqQAVgVEc5MsGydL2UqRGuVTB2xjElMRXEONI4IhwP8fsLu 8QnD4YBhvweCRwYhKWA2YVVSRo4RJUYR+hV0muZpG1aZhgENA3BoloWs1h8OHn4cMEwTxnHAGEZc zu84f7wjryuoJDjtt9Lv3KYw6LxXkMAdyN/s8i2zrjJH96px982FBFMCAkuzr+wYkrqjwDEm5I8T eF6A0QODVzZByBBQaU0JUQOTe7Blj1SftLn/gBb0Woq0Ew6i9bngQdMIaOdUKu0e2/lpIkjAZQfO NXVbAKquUDHeJJzXREheZhz/+IZhGnHYTdiFgDV5rDlXhm6uA6NXrs8Rxh+CB3MQC2ZuI7NidzYn hcU9skLdr8ELnyQx42fHGPcTHr5+wen4jsvbG0rmyl/ErUh1DpwzoEG4JYobIrnRfnvAULrvfpYF 0pvqRQao8LuRdpsnXX3AV/EXW1cib/ZDp7UXGJLscKxrpE0GKS12jRTwOM3SIK1XcvVelsFRWABL oVY0UTSpGvtiAT2FCpxTYAEJVmZYuX5qw1QXCxhY04x4WbEcF6Q547ffgd3DHmF0YKexeaTryi2r DdiCA5tqczWzvae9D3WzbTzgej07OlLu2YH5+27Vzw9VyOsDO9DL3RvYQMFUeyPUwZmgA4FgXUC9 omp9jKI3cMEaE5Y54nKJOJ0WnE4rYoRsRGakLBYIT6O0GjEszlI4y5gVqzkKWskRnhHg4B3B0QBG AGslv8JiWQAXBQakYwKcE3CxJtloYoWA+JLrPEgAHMMySxgMyXAZJ4ILXkzGOaKkiFQAzgVefX21 tTcX5JSksFTKAiwshdUPm2ZX+lQ0FR3gjpoMiRoLJbXOOOcBM1mXBJ8zhnXV8tQRaVmR1gUpF6CI ZaWkrKCGazwBqqCVZ/uyBTlVnFUmawRjxbmoUTxuI72N2OUxHcTWe3NMcJb2QVCkaf5Wbko8tzmr DFtBHYKHCx7DOMFNA4bDA6bHB4T9DjSMYM3sMetPNd4XabHMKYvf3Epcd8ypgiU2wdH8vmAJVBy8 hyOPjIJcCBS8WCwm6WsxjiNyzpjPZxzfP5rFB72/V+ePmmsB3Zz92e3+maZpbpJrIeMg9T4KFzC8 7CuQNNiaF8ynM3YPO4RxQFYFgogUjBbg6n434ymMHmwwUON9JJtCwCx7Yf5hGOCHAcyzzYBN/u0k VLptGQqioPRPg9JOm+Nm3mWkZcXH9zdMux0OX54xDSOYovKoXBPsYXfjZm62uQ3eowRpcNYXVuvn 3CwvpUiBLXJOKwBDC/VJquTu6QGPX79gXWak0xFgbr2WUOUboGsmfzSh36amY/L31oVvgzt/dfQg Y6ME0facPy+cmlbexwH9fExKPA15Xg+g8gci4VEGzMjmqlbvpNrIz1JXxW2i1i2TlVrXiDUFlpwF hmq8kgO4iCysabBmfTMSVIXtXM7I6b+wzCtev77i8csTxsOgYEQVfFP+7ri9GvftQQd363ALJj67 iZ33V6DEZ0dgrhi8HoKslOGSuEZMzgmNOEU4KuUkIx6EQUFGhpWhlo6CDjkD61Lw8THj7e2IFCWN k+GlWVjKUuPEeZQilhLqTaGKPsU8p//ZBiVBlYUIsTDOl4RpIAzBV02qQAPIIH0qQpCAtIGBITIG YiyxIGaJWjdoI8+USGGrx5ElCR5wAi5CcKDiUXLAwMC8rLVei6BHrY3BDM5aSjsngCLgHbwf4EJA CF6AjTLOYtkb3eZsAVLi0uk1JzGjOdDoQAgIDPjdHlMRMMExIcWIEhM4RfFvxxUlJmTtj8E5S7U1 bZwmnThlQzk194HV3MkAOoOzcEXZ4PZpHV2vYev+bAGAep4ReG6QtVrP2qki9CrQFJqBd3BemLMb R7jdiGm3w7A/IEwT/DjAjSMoBMTCbRMak4HEz3Au4LjKz9IYDNTVpsNHc11p1EXdlQTvlFmRGF/I e4y7CX43wgWPcRwBAOfTCcfjEXGeNTWZzRLaXtYkKHXPaNO4ObaC5Pa7m8/ki8rEt6Ztjb0oYgkQ bUxag18+ThhfnzAcdrC+G0wOjFxjYD47bD/cBQcqiHOSxn3wHgzGMA4YxgELbP2NpuwH3QSebYMH t3hn4z5QxsZKBzLAgvP7Ed/CP4DgJB7HeyxWw4LbOLbqVntIDe68Sk+FvXIH7iRTLoIcYXCTCiuh +wLCtN/j+etXxMsFP+IKXletqmpg1oovOS1zfl84NCCwpZV751x/9itwsDGJ23terYd99lfvZ1Dp NnenPVCWhXVPtrooNah98y6mJ2hskAG1jl5ALRbE7lHqvRUwsAeoS2HV3lbKlqsFyoCFMsg6Hnm+ FJ07nU5Y1hWn0xFPxye8/v6Kp5dnjGOQvcCStszEFSxzN0/VIqYKSlX8OsvSz44K8vsPfnb+T9bP nhVaUl0PMqgi8cbg9aZ1BPa5Vt1EABCUsDJIe3qIQCcQeYQwYhgyvFsQS0RKJsDQzHkQkz6Zhgiq lhCW/4HA1RwuefEaw1IK5iQCfBccpmnEbgoYwwigSDGsnKuf1DsPz0BwhOA8hgGIiXGZxXUjWSEQ SwebWUoitWxhLXZGLAcDWFNupQ27pDCCEpATkDTosmNJEiORkHNCWklT/JyaSbXLnjWW6hbOmHSn fFUtpuqEZgJzAS440MAIWkWOWYQn51zjJ2KMYjVYViAnpFikQ2yS92YtH27KaQ2a08ApKqppStQo rCiY041q8QilQ16sFhPuzifnt1qQvQ4AdgT4ocau+CHAD6PUvB9HYApw+z0watfLMNYOgKvOWbOk +Jr+WjQIteSCEhNQ/dhNDtSN0wnQKpA7a4LzoQIfPwwI44QwjaBBgnqdd4hxxfl0wunjQ3vY5Bqx fsfW0/bfT0zUbVRb7a8/7272Sn9/NKCnFmD7BOJKI8znEx7mBWN+UOAlKbvauPbnB3Nd86qRdtYn sID7oul+zAw/BIRhwAwN2NXZrZbNv3g0TgMFV3VoAg4BcE44fn+D8w6/hf/EsJ+QqABOov+b5aEB TJm/5vrzIcBbamqXodCDOpuDnKUBnvcefhhg/JhJ+ttMT494+dvvWOYL5rcf4JJgkMY0W4Jqur+y HN2xZvxK+FwDgs+u26xnByjuPfveQdVae89awe2N+8Jq/fUArEx4s6aofKtjY5jbzjkn8YEwv4XB GLmmWF8jtcA3HUJ/dyKELICUIDqe1NpxCi40RksD8clJHAY5El2MxNGVloiP9I55vuByOuHy2wUv X15xOOwxjAOYCKmopdk1haPFYDRrmEn0Nq/Gu+yE253zs710z/r5qyNYNTDSAlhAdcV3UOOKqDoB RrUJ2aAAQwWXnUNyfQgebj/AuwnejfjxdsTbjyPmOcLygjNI21bniiiN0fVlfO3ZzKRmIrGyFBY3 RykFa8oYEyMWxsNuxBDUbcDiKhEzdKrMYfCE4MXaEbzHvGQsa0LKWdLWigTtAAIAZCDqX+xXEMCg 5lAfPELycD7AxxUrZSBnRCvOY2yy5GrRKAAoSM32kIsU2fGuET41IGZBnY3I5PliPCBdyxaUJ3iD qoXDNppgAYZjCQh18wpXMnJi5LwiR0YpETkxwBnrmgBOkOnLMg8sFSuJCwonQz+VEE3r4CqwoPTW 9A1A44DJSxCcSXZD/p4A5zDud6DBw4cRfhrhxwFhHDCEARg8VpL7ZFbzvoJhs1ZYb4o6/zkhxSTp gaXFlDREUSr0Npre7IerbVmUw3gfpPrmOKq/1sEHhzUuKGtESQmX41nAjHZVpcIVKLb73z82mkb9 5OqcO0zhp1ojUccslRHV2BeBxVJddQavUYIQnZP8Bf55nHv91oBPG9TmbxO2HrKPBufhteS+GT1u 72sI8ApwdDKldwf1e9aCb60uAgpjCB5LjPjx7Rt2Dwe8Tn/DFAYsmRtAZW4mdnQACVyfFUJACgGs pcENBJgljwAtEFY2AENixQRYMBeMIWD/8oKHjyPW8wUlZu0gqzFtqvpUpbCbo5+t+V9xWdwDC//q dffdNfa5KkVXIEVWiTfA4jrmQ+9SaaLFJynvYwEWDXwoZyoWZK7yV31NrQAXWs2UmnJaqtJJ0IJ/ ReIvJExP3PkORQGMyDDSlC9izU7RIo9Oy8XHOeItvuFynnE5XvD7337D0+uzuFTJwRFp74+2byyV 32IsbML6PdVBo3usoh2ffP1X3GUSc0FifbAAzuqdsc3Ybq0m2/4BtomUQDTwkdlvNDtZ5iJpnTsP 554whBE+TPjx44jLOSIljW0wzUR6r1eQIjEIJlRZTOGK2oxZMEnp4UyMNWesJSGCEXPBbvSYBofB S6SCY+l5hyxBZd4JMg2OwNMEeCCMCUuMuCwreJWQX+ccorpFGEqQFu3cWRfYJjgMUvLcewwhI2Zp lCalpFFNXmZ6J2fxGYwSNdPEO3Gb+AAKmoZKMqeoG01GVJhRIAToQLJnSBgY1FVl2pUwI92ETsAl eZZ4BGhgJhcMRfzCJYsJdtSfnCwzhZFzEpRfMlxOgDUoA4MKt+wT/Wm0cU2QUlp6kO3qJLPEafwE eQ94QhhGIDiQdal1hEzaHp1QGwJJgCejt7wxM4JWEOWSgVKQowRvcimwFnui/xhtbd1SP9tQDIjl aQgYxwlhHAHntHS7WK6WWcrLr7O0Lc5LlGBIsKY4YqNZXFl2O2n5WQHl7fErAXIDQPrfxCSof2s3 2pixnC7YXRYMj4caK8EaG/XZM3rAabhl87zOhJBzApWxBow67eFTrzWgZ9ez9PIB297QB/T+L5gr azOwuocMnnOWoOeBCLPFXxz2GB8fENT1kI1zd49ogk4EjoeA4iGE6u6pc2GvrLEfBPH1xxi1sKCr dWaS3juMIw6vr7h8fOCyRORl6bKTWiOsZujfjqsd3eRfjeevHJ9fx7YMqOJtI0+uITHfnPvZISKI YTUwZCtsQcjt33XA3R+9ZaQBDrNeGa2RXsZkCiFXcOBY4gCdKmwMsvAwgEnrPCmP1oC2FoMhHcBJ x1msirRaJLgw1jLjW/yG9bLg5XjGy9dXHJ4e4IMaAXTkm+BOA618b317tPEzgPjXwOO9g5kRJGGk gQtHgqBEaJcaDAkUycCwOAAVVHXAVDQlyEHiKCwtTx5WWBg54BGGgKfnB5AbUTIhxRNSTqpR6vOY 9dk2ifJfgcRWtKY1FhxmJEqSdeLUqpAKYppxXoDDNGA/BUzBIViQWh2f3CszYy0Ri0Z5+2nEwzjC L9L4rGj8hDAqS0/s3BwEtOJc8ql3DjQMAjB4wDCMSDkjpoI1RuTU/LJkDJgkRqSApZZHSshOBCyZ FuydaDkV1KmJFUUzNq6ye/RvR1JeWQSCjNtwI4MlZ9fqkRDgWHqwSBCcdFX1wkHByEARy4WQSobn lk5ZLU+qtW3iRyotN9cCQ96tdL0BoBHWNQqaCOyFDhJZdkuzIQXVAAr3GjhJ3EmRKGzOkq5LLG40 Ysh7OcliYAXTphtsNCSizfsA5hYT98A4SRBp8KO4cZw29CpSPpxzQZwveH97Q15WgAsG56U2iO09 3AEV3fOp01z+6vGzwD0TwVAtrKZ3qjrDhcEpYz2dES8zxsNex3h/LNdByooLdK/faq9iOGCklIXG FNg5dSc1paWNd6OAAGgR8f0zOmfT1by2ujLyrp4cSoxwwWF0hMvHB77/V8AXIgwPByRXdP/Io24K m5EGkaogCcMglTy1xHk1qbNlHbT9UFLGSqu49AalfT9IkDGA3eMTnn/7Cl5WzCkh5STGdrVqNnfy p0uCqsDjViD/mYDOX4IR7qfDrEVXJ/Tj6VXtfpDX465nKW/hn493+13dyaj8uR4NWMg8iGtpC7eV N5VmzTAHN6u5Q1y6undIYzQ0tV6qNBuoEHeslFkonUWdK6h0ynxSXvEjJlxOF5yPZ3z92294/vqM cTfWkTOMBu0d9T0N9G/RdJ3Jnmw3caLcz/a/cggBBImZ8ACCaK7OwYdBNk7JKNAMB2jAJXJtVgUu gCtgsjgEgrhGqG74ylxUkDEzSk5IKWONESDRRIfiEVdGyouY/TPrcwzNNiYBSBdUW2TWAlhgSR/K AMzknpmBkrFmRiwZcwp42O0wBofJe7EGsHY8TQlrzviYF8xRrBO7/QGHwwP24wCaV6zLApA0WUq5 aJVBVIYpBXo2zhJYdz+pNQH4MGiTI8Y4RMSYEJP8Axf188m13vToIs8sMWoLX+lKmdWcJgTrq7Yv +5nBnA2rVHBh69AIQUEaZKOwnacCxXoZSjVFBSRaxwKQGBGCl5KzXDapXZVE1SLV84Et/5A/BPD1 pYmgpbp1XQGQ1wBdNgZPFWDVjJciQoO0qiMRCYgoBUmzQFC074MOhszK09VM72MUDAD27Mpo3HkP PwTQEDBMO9Ao+0AqjgLMBUkDagcifHwccfz+AyVGDKzakDH6uhrbDd7P5b2930zzvwYdnzHkFtMA 5RF3RpML4rIiXhZQLnBBSyeThbvZ3N3KN+a2bu1dWuyFROsXcJHYHB8IKFozQOvXhKDEAAAgAElE QVSFtN468r8NXzQa6N6xF6bomG4DNC0c04rx5VJABQjOY40Jx7c3hHHEl2nE4CWmqkDWVYbehIpO XQM8JO4RC+6sY7VXYBujDDKnjLhGDI7gLA1fgXcYBjy+fMF6PGM5ncGLWti0rfjWKHG7xi2m4XZd /oq74x79/Mz18rNzPn32hs5vULauY3NxtGc1WdHHXbTfGYQ+jm9r2TSw3PckMQWFzXpn1lE28KuA 33HN/COwdMu2FHaWQoI9IK7lwh1kHGroKPpc56VswBwvtSfNusx4+e0V434HPwgwYbBaPUhlrVp2 dLnZ6P5qOo0aOmzyv+0IhAAi6wEiCU6leGGWjpFSROEEKeyRZFmKVlh0ispQwBCTH2lcQusbIIEu hQtizIjLisscMS8J8yUjRomsHkcPIINjakxco/rZzFXgusQ5Z5j5ir3lkLP6HsnkizzfBTBLz441 J8zpgt0w4GGaMI0BrhSktWBeIuaYcFwjslLAOWecY8Jut8MYRuyHASkWLMuCsqxSJAvYCIeCslnE mjPe1conchiCdlUMASmJyyRzwbpK3Yw+yrwANXvDNMicCzIlEZ5mxfABGMSa4chVpqVY/w6aN/cI tQHr2ol26avWCoYSK9V1NkRvWUcg9b/XHHNloQQozm9rSa59rz/Fr2kF2dT6xCZ8bB5NO2uWCadm YbA03BIhpkV12Ko/agVRXdua8dQVjGErloWOKZq1QE0J4npSJuUJPgQM44RxlGwQKOgqaAwwxoi0 Sl8SThnzxxHpMmvhLAHyvhN2PcPsSEk/+yRu3sZ7xYd/JTSurTLGgppiYPfUHgqlgNeEeJlRYgIN 05a5m2m2s/C0ITZw0QPeeq0+jwEt0KbmaU+VHixankDVjVTjw3gz4H5y2ln69QZ8oPmkGaV1H9V1 WZcFpx9vmA47PL6+gsOANSYUljoERBBLmPNt7myvsbhTQwiSjm5z0lvwTNioFSPFBPIeg5eaIwJA HDIXjIcDDi8vOH8cMeeMnKKJvGb5/ERQ9ML3ruXqV1YJPf4MELkJtvzEynD9uXISbD+5tXha1lDj RdeAkuvd7BktzqLjd4BaFeqda8DlBmAYPzWTYmaws3uKVZe4wFLkHUuBL6cVZB0xyFJYIbTSaL9o s0+Re077VjG3ehpxZryXN8yXC97f3/H621c8vYoVw3fdVpmlpQJIWnWa/GmVUAHbBObgByxG9M8D zF8dQRqMDbpAwhS5tkh30oSLEkpOUgmzOABJN5SZGmXSJE4ighC0yqHEFZRUMM8R5+OCeV5xuUTk IsWriiI3cg7D5OHChDUKYRStEGlBKgQ16XOuVQCZoRGo5rt0IBcEvQEVaIhQ8CAG1iUhZsKaCeMq aZp5jVjXBUtKiERgTYPldcV5SRjnBQ/7PXa7PcZhxOgI8AHrvGBdopi0iSQWQH1sZiGo+8ZiRiyN DPLe3ol/zrOUHZlGRkwJKUlH1qxBpeIAMFoQpiV1ZBicSYCGW+FmrxknlopL1ZVScx0rOocCuG4r c283oCaEAcAEvY2hChLXnc+CyKseK5uMNputgMr2cyvjW1E9uud2v3Eu+miurjcH1jx0iacRC5mU eM9F5rDkJP0y1AzZlxG39xBSaYyqhoaZIGy+EjhyCMOAadphmiZJFQMQoS49BZY5S9qvYyCA8P72 huXjDKSEgFZsq7H96zdHd8ZfP/5KUKe8swWwyYtvohSYxW2QC9bLBeuyYNpPcE7inaSyanuXjTDp h39PUdX7W0yQtCgXwGrmYgkgV0epnnuDJUyTuwOaqkxnVOHeu4k2mr2yNOeEvuJlxo8//sAwjdg/ PUmWFSRFsGjqdufUqvd2KpyC9yjDULNH+mH3wApEyCUBkeCCQ9AsNDFxO7B32L+84uF0xnw5I6WI gSRwsDlo//eqodfWh5+BkL8a8PmTp96S/MZCgW4xeXtdN/9yRrWn1e9Q6VsACtvYTVGqbFqJhmDB cY1+NBBUPi+WJqJ7v2hJca4ywdJVpTEaA+wlDd2JcuycV+sGg50GhLJZIKRz1TklXC7iJnn88Yzn Ly94en7CtJ9EseqRGWsZCeVxNQ28n0c2gN3F6vxJkHn/kHsE5wexWrChPwfvRzATUgGkMqf0+piX hJQZ+2nSGhRSetuRtotlACyVypghtS3WhPmccDlFXC4JMWbkLC4UeU3d9JRl8okQaJSyzTmDsmR0 VMTd+fOdThZnnUsCSK0v1iJP9DwxZ1UZ7zzWIl1WsSTRfgoDxSHTgOy0eZMiXAfCnDLi8YRwnvH4 8IhpnDCOEwCSDq0lgUg6hZZSmqWNAMsqscMQtxEzw+hRfh8GCQDNISCpYLRui6loqW8FX9ZxtTb3 0RoUHKWBEwMNaGjdDzaQoQQHM6eRQEv3CV1VrcDIx5j7FRRwpT9L/lkhml//tOJN+sx6Z7Qsj2ol EiAhGhvVFU5xFZopBjASTF32BpaYu7iFprUQWWAcNMhV5oS5xf447zAELyBzmhDCKJouzLokcR0E 8avGGCXlGQTEhOMf37EeT/ApS7l6FotbqWsAVL/8ZrteaYKf7f9rYfvZWt4IXzkKrKBXK8jUgyxP hJIz4jxjXWaMeJIeOZ2W1PasAWyuWjzV/z4fN5eCXApCtRqJH1r2/N0Xkv2zwUH3QNSVwLoDMETY QAbDEmMUIHR1eX/Hx7TDOIp7RDo8yx70TmOPoJa9Tv45Bd99a/bN+qo5W7wrcr8cGTk4eOf0eiGO xIyw3+Pw+oL39zes64JSshqRrwXtnz828VAdgPirAZ+fgdc/+/nt87j7fzsaDxJatf4yxvfb1Rsu gg4NbAHG9Xf6VzWAOHODk5KGaxb8ArWcqyVL81GZxb0PApisy6oCjKIuUxLrPhNrbSOzYJhEcN16 sPajKvh4e8f5dMbHjw+8vDzj5esXPL8+Y5yGmj0CoLppGNJeg8gmiOtL2p9ZM+Ou+xb95YMJQXLJ yd4eAEktdHIAO6TMuCwJp9OC83lGKiseDsDhQVwKnpyuhdfGXQVxTYhrxrIUpFX6eaQElAyU7ACN yzDbqCyKCPMCB3hJYnHqBDcNVFwCprUWrVDpKuMyWUkFYO3IChi4VeZHXTly5mZFIAnKKyRujapX sBdwyuKGiKkg5ndM44T9uBPGNwzih44R8xzhBwm6dN4rYLBsEigDbqDKOKXRr8g9qS7qg4eD5Ppn DQbLJSOq5pOyfFbdSHovCb9lZfAQQFYKSK0r1oQJpCmeJD7DCjC8Bk8agdkPY7idbtYqxvXvYaKw tJ/cfpoGWerf7SczKnC9dz8ubM4VrdFhQce1Cw5SUldRNUvLODyhdmTtXmhrMkW3FWCaD4GUyZOW f/dDwDBI51+AEGtGhcb96JhKzqDCCM4Da8Tx2w8s70fwvCAUrgGkABrg65jjXzE/914M2SNbhn0r Z+8DjAa5ulOBOm9egVm2omz67n22QqX3DlgYuPgc+ahlQa/nInExkjVtY+PrK2qlnupb78a8ff9b INHApZ0phf+qy04eL8CwAHnN+Pj2HWEc8Pz77xiGAfOyACRuyawdMm1stlskuI/gO2vive6cNj6A gcIoS0SBhx9HOM3iyshi6X18kN4j8wXldFb0m+01fnncs0D8LNj3rxyfXf9Z/MWvAEZvUbp36wrO 7l1PJmuo7gPu9qtYzrs/NcrMYEuF9i05T8mmwPCcZZDIV6xlC3Q7ywmAykdSN5oJeUmxLmqV4+oG EdxjRSwNIFCrEUNA5ozzjxPiZcXp44KPtw88vz7h4fGAoCn6oujLs7y6eqQQZOMbTUlk3IH9/9IR SnEapQDVDDyICSkBMWYcLyvePy44ni5Y14TCjON5xvMCPD/t8bAb4McAFInPWJeIeVkR14yUAIcB 3u1Ao9PCLxlr1Da0wklugggLCEzCyKWzpwNSqsVrxBbhpH5JjW8Q4SI9TLICDJs56YZYlGlZhUay JmLKBHNlgBCG5rQ/QJHKg955EEHSXC9nXJZV+4wMyN5hXQvWdUUoXopgea1ToUFZZnLehnShSQVF zIUTZLlF8AthAcE7BAwIOYNZGowVBR3JOpVyqe2kpX4IKogyjdyr7xBmUSAFGiBkB/DAyIRK0HDN 39hUa9tgt4RY89A/ITrTgq+zoYwhOKhZ3KwA9l0RIGVWiJylRDdparAjqRNbtPmYV8sDOlZhsK7t Hwty5SqguF5GGiDrNa5ihB8kCNhqZ1i+eZ93HnV9wEWCHJ2HL8D544Qf//gn8mWBL5qqyMqcQOqy UkHzydzdzGXHRJvp93466M21coNboQuu+xEArPqfMV1ArC2pFKnsmlucSn/fawpoz/tsRBYEp/Ez pYFvs4Q0gLJJMEadNxUivZC5dY/oOhuQs73AwthlPSyploEifMgr4F7nBd/+8QeG/QGHl2dE5yv4 t/1aTdOkwIKbNug00+s6pqfNkSgXDpAy9IgABYkd03tHZDhLTT1+4DxL4TvB+rkKp18dnwGM63P+ /zgaKKa7f38+pvvfb97lChnX7zYAw57nrpiRWa+Ua7DGVDgC2G32CRHUfeIqXYoymUFaXE7YZIvb qtU8SV0hFTyIy1iM6NojhQgeCkjhxN1sLjXvNF6rYF5n6S5+vOD9+w88PO7x9PKEp9cX7A57eC/d dkV50oZ3EDqvje+Ia5bKfxNbAgAC28ToZBcu4JRxPF3wcVxwvCTMc8aaGLkMKJCqkyVHpIWRn3d4 evAIlDHPC+bzGblIy2DvBjgnVgoiYBgkm4EpAZkh1moN2IMsYAEhVzuNmheJhcnXzAmdDBbf1TZC ALCc7/+PujfvjxvJ0YQfIIJ5Sbarerpnvv+X29/OdleVbUmZScaB9w8AEUFm6rCrenZfdrskZZLB OIEHd60V1SVyljbOxlAG+VjI0S8B1SMIVMJnqJ9HFlNlcVANjWjJeDaGV5hAU0TKi6awNmkmxKlF cXjehdXaScPkcIqkyXmqJ4q0udB5DKTJyoLVtC2i9RjcbJJKacTOHRQ7we12amfgROadDIIwkPPV 8jJ0jQaZhkOnxQmorpODE7/CO+ACMIa1EWId2NWS4aaF5vcgqqmoVvyOXCtT1TSiecEULBLB1lsU GFU0HYrh2bWU7u+wMavpI2hNixgROA4Os6ZNk9oSkylIU2eq4gWuTAAJzOAiuL684Pvvf+D8/TtQ 1P8iGJPWLLNkhO8eNNgQ+3e+H+fzo74W4z0yEOLteolRHq3GWZFTUh+CaPujaX/sLDVpcwAb0nu8 6g1pODBtPieT4rUmiX1n4IBbNFnHu6M93u8dNTTrS59SBqCDVOCr72JpO8O2k4KM68sZ3//4it3x iF2MmFNCqdUqmw50RRwydWYfomoj61AMcHu1KKaiSelqSJrnJQatsosKDox4OuL06bMWlEtnuI9T D1J8/3pLg7H9/f4c3s7ojwKSFWt/tX3TaQ1gcAtMuoZuAxZktR03l5vANyevnW9HidQYvniSLakG HMT2IFkmBUIl9z1TWtTBBZuvGIPZowON11lma4g5BjODahc4XEvnIKlWsSg3pen5suBpWXB+fsb3 r9/w+PU7Pv/6BQ+fH7E77jWiLVjBPrKd6aAddkzoFdPjD16RwBaDX5GWgmWZcX5OeDknnC8ZS2aU GlEkoLYkWxHzklBSwrJUnJ9n7KITgYhgYZEav0vNhs5BM2EKBVAWgNTM4NYOLTplvhgmoagNkkAc oCE/6ghYZR1NwUb8SCoiWEuoV80aKaxqDO0Ou7JC21hvMZPQfWOSah18zY2xKTMJDQr0WiOWz4Kg KuNckHIB52LOlQSQqtXbO0waqm0To50LNzlo4itjptSJoZjYxRw0l8IUMFXBoWBVo8H7XUvtDLsx VJN37fyQ5arQ92tiqra1jV/4IaltDN4vm4ZX5O6tpqI2TuFMSwm4Zq6rDUT6cz2Ur6uTm3LKSbgV bnJHVeiUazu1Ex930Gx1KtjAg5mMwj5imnaIMTpJs0ak6cqI1DenVtUYuXYomOEGVfVUeV7w9fff 8cdvvwEpIXgWUAdY4g5fhCK1JUbq+3ALLjbM4AdiyG7UxXeum7wNqwb6o2IhqTVnUNi1DJokfe7H LS1jIw1HrwUDv1Ec+tv8VJG2VnSnmbF7NLaH7extxyJm8x4+IkA8L8HYkBHyAEaQjOev3/Hw+IjH v/2KGCIE2YiygxmLIrE5dTNJ4AAJYkB0MBsNE9E0NAIDcRkIWnK9AqbBYPA04eHzF9SnF5zPC5Ar VF3/Kif90LUFBx8BpqvnN+2smf+mnTuf3ev5SCKbUDO0OTLJ20vGndPbXGkybgFGm0VPQChoGsYO chVseO4lGemFmTss9bQJcdobZmvTQYeVc1c6q+2x88AilumzQkwTDmJzOLYEc2xgR2/DeTnjer7i +9dvODyc8PD5EQ+fH3A4HTHtJ8QpqunetZWuLSR6jTT02bwzz1uyEUkYsgDzJeH5+YyXlxnfn64o EjUZFUVUYRQZzBW2+UsF0jnjfFmwmxiH/Q7H3R7M0RzzYN7jtqyqo0eMMKk3aOXDXJFKt1W7vUo9 pFknHOqbQSSgIKgloXgODgHMo0Z1Al5sy36XoqmggQCSoO9mM7mYxsT9TrQpMQnYEZypxUiTWPV8 B74QnZwJgBB3mvSVsjq+FU377Zuo5Np8MjRHRWcmAjR/j+b0ZhKVOp76QUHTPDRnAwNOHIyiTVET TJmElEvuGg14DQdljp6wS/nsqLL1jTQyegMaw6ZabTJnmhv5tEmANl91tUG7dEgULJMnmolCCbRr KajNO9nnAHp+EK0nZJIDDKGrVsbH474n0X1PQkAIrEnKQgBNrFo38pTzzjDaTKhvjoM4UR+gQAQu mpYZQsjnC55//4rv//oX0vmMPZM+yd6KlRbXDe8JKduUdClsWI93mAaNd4wcawNCRibd/E3GFaE+ TjfnEZnTF0c1o10XYCmIO2ARPdfEwbRMw/oLGSa0cza+vMNXuERaIernAGnOy3XYk/5AtWgWd1fp czQwHZtb4JZpwr5rWjQCvCS2zoEDf/u+CgJV7ImRLle8/PY7DocD4nGPAg0VFZDDS40gaVqp7o8f LMfOmPui5+0hx6ZNfZ1LBrIWSIT7TEHp2P7xAfVvf8P16YycK0jyAJZqAzgKzBx1y10O3lnw7Tz9 iB+GJ7C70YjgPsC4MVlt2ltF89xjbK6dktHTZdVC70Fbi9tLms+Ko+N+5l3IoEotI2efKwKqaR9g XWw4RVbnS+s6GU0fkgWSlVY3otYJadX7KsjyvdQmjBAYpVIDGN4OWRRLLQXn+QXn72c8/f4N+9MB x9MRx4cjjqcjTo8njXSLXbASpuZoqr3fQHiyw+Y80IUJrG+L15cFl5cFT9+e8Px0wZyBJQMIBAqs zL+hMZN0BRBhVESQqDPbvADXKphLxWOt2O8CphgA7t767JuGCdGc4ygyuFQgVdRFk0iJAYVaGVUY VULXZpARX1agA0vhDCvIpeuiZZG9gJZHVJSc23MUIsABTBEQNq2Mz0+xZEL+dzcbGfdtG1ycMPl+ BDR2OAa1+ediBbFK8xAvqWhK7xCag1cLGw3cRe0BHXt9B8+YCGOOJNI0Q0Lqc1B8HhzdE6wgUmiA xJGqR6KM1RulGHAZHEX9HwlaLD43Yt8ZL8F4iEte9iFh+NOGt+Z1w1GXaqU4qAEeuMaBqBN/6Y06 EBSB7gfTgLVQ3AEEejbNYPNPTO1gCpGZ0rj56FAjNPp7D3FNjTEovlM/ikhayi8tCc//+gNf/8// QXp5QYT6vhAqhLsviR9hjdAg9+dqGpmtTPA+kR8Jp6Chqg01peG3kRyPdMKBlZP7CoJIQURA4IC6 LOAlI1ZoJVnPplp8acgAnXVBOjC9GZkB0pVGqwqKZVWt5nxNFk5XbY2dXwZo+yNr2joQ3pec239s mqgTULIU9vY8QUC5YBcCCILl2zNejn/g8T//jjAFZD+rTT41F1eXrI1fEDNiCCiF9fyJGGEHYGml 69DXWiuQMygncJiaul0AhGmPw+dfED99x/WSwEtFbEMzPZvRr4Hfqe0f6BrSOwziZ67ORF9v6J5f x4/cfw8srvat3AJJX8O3tBv9vx1cqCnD77FKIgYwmuAH0nUjF4L1HhXUajM3NKHIMZ4lWGxE0bJ5 Kt3XfFHaXm1mdRIBpBjAME27qPlE+xqsIJq+j0mBfS0Jl2vG5Y9nhBiw2+9weDg0oHE8nbA/HsCH ycotUMsYvK1EvDkybYJHGhL/+c/fcHlecHmeUTIgPKGJwgLL0gmN4mD1Vq7Vq4EaSRWt0zAnwVwW LDnjVHY4HqzsOQGa4VNb8kyPIVSTdJSk6v8q5pwt/8XAjYYDV+CIL2rNARKoStIc6VrQoE52pAC1 u2jaZwhbApSqUm5wbYzuSmJbVBm8uR2RWv4C1yQAXRXXiKmbPAlaCtxrrbijH2CSWNbIBqJWRZFD 0OgT24TBD8MdRizVyxUNBxqdkfsG2KrCQ3B0LQghWH9q+ywni3Rwk0oLfbXPjYvLeFB9Aols8/d+ wg46jad/tSs3l1UqJVKHx8aMGoBxZ6q1tN3AAykwdgChJjpu+QZ0z3RAccvslKi4hqSvmc5RsfKf Y0hhsMggrsBEhHy54um33/HtX//C9fkZwQh7Ttm0JX0CvA7MGljck76215/kAq9cTWgaWH/DtO3N GtMvhSBZU6kDCnC7CQMNkL4mKd7vgDJfEVFnUVaTXq2l5QHW20xGbWpdwlhTYXt9WLXfpG6MW9gE FzH0XDHFiFQKvv/xDeF4wOnvv2ryO0hLAMjsTtp6dvSs2g4zoYJKAbmJ1w6twLMOdYZYa0FOCTGy hb2aJoQZYb/D8fNnXJ9fUPLcNLojuwRg+9X3GzCu6P8frntOoDe/D8LOvWdfa/c1v5J73/EQftpA qyI2pRnkKf9r+76ZMgZhp/9T/wwSN5FCzxe6r4bzGvI6JyQg1nNYrTYWzJ8DLog5zXSwbLQnl4K8 LHh5eUGYAqbDHqfTEYfjEfuHA3anA+IUMe33iFH5LJsQBhACDWvg8zWgDIIg/v7bH6iJANFMncqQ AQ8LlFK0SBUxRCYIB/VmJVdtEgqxFrUqFUvWf+dccEwZx/0Ox13AZD4TEAGVaslE2NTAjBCBCFZ7 tQAZMhyQkWENjJwIHpAKjvCw1Kq1MDs4gUr0IE1IQlU0XwQqKldIFCBGtWUBJrV2+Ye4S3ZCGqWi abDF7OourfjmM1MQQeshkKLcAp1Pl2ic8fvmzVZbAjmrFiIEi3FXdRi7KlSGvnUwORD/tfR2TwW5 /c6JIADV5qD3zf9tNRkj+NjaO0cb6/bd9wj86tlh495IH8PnY9+3B5UNPLiWYqu9UOzTJcrOyruD sGd/3I6z1tpAC9kadD+LinlZ8PzbH/j9n//E5ekZqEVtp1UQwU3jQ1D1IzuxJ+qSZe/l69efzNd7 4xB3j/G6VEZovhhjDgdUZfyeSh1EqGXtQ9B6eUeavHdVSEvC53WGqkUKNcIJ3eWAMfymKej+E6+N +d41ajXIxg3Xhg0s2gFDqQVc1c49n9VDf3o4YnfYI3PFkjQkla1CskcwtaahQk2UqLlQxLQcVcwZ 2Z3O+7l2B1qK5lhs/aoEcIh4+PwJ89MTXi4vkDnDFfdiEqdrdVqag/+Hri1deE+T8dqz0tZtDUK2 99/7rtEs/fLN91dLfKXCdW9PV01PtAOL9h4vUXAXXAwAw0z/ZBoSITVViOXF6DSwqraCGSRdaFce qH+z160x4a5pXMz/WkBIiyDlgsvlAmbSMPv9HtNuwm6/x+6wt7xOGoIfY8Q0xVZMcLVu7PhBEDkE lOQyisltBi70UAuKkIELASxduKrX1Q+jQkM9ixUuK6iYl4xzmnFMFY+nHR73E3YhKOK2kCl3UCEw IhFCBAIFEO0wJ5VU5pRN7a/1T5Su2cFzT0QhtAKvJBBOqnWoVTN5GeOPxIq4WFRSsFwRORcgRlCM ai4Jsed5IPQQHt+EQCNhTcJkdz5VXxSXtAMIFNDC2JgDckoKEIwotmyjVeOW6yD1dOnbkum4bc0k 6xZ2bRIiIHcTCY0HwDf99lC1DWJEcAQcW4es1wCHm65Wf+uDDeHeI2xbcNH34apnCtiGDe1an9Uh lrr6fttQFZ2jlk3cSTipFkFBH5mmrNqa1J78zMbl6XKZGZFZI3aWhG///U88/es3vHx/AkvBBAJK BkpFDM400Z7vkqSD2JGt3J+je9c90PhD1xZgyODsaE1r7Y3OwFW7oFFJwT7NticaYPsgk/DXiO1h MqCnJs1kyarWM0Obh5vmYsMcPjofrg3oA/b/uElDibNU1WART2CpuDw94fmPPf72X/8F4oAEC503 nzOVbXxv6TwzCAga5lxtHl2v0fOsrCGmFAUYHDXcHUyaU4WA3emI05fPmL99Q1pm03qwCWZiOV7e Bl8/er3FxP9MWx/Z61sadg8ov9bOa3tj9N0YtSDafnvCBEuMEt3YOnr100GoMW9nr4X0KsCwHCtO 27Q/mj7cq1wDfr8K1w2UkMBTNRCxavOclZmQqnvQxl3d+Z1ARR3405xxfZ57rqYQEGPANE0IMSBE xjRNmKLm0XDzcogBIURYBnzEf/znf+L7Hy/4/v2MUjPitDNEY6q9KhbJUS3qooIiGZNiFAgKuIEM dYBSwky1Is9ZQzZLxcN+Mi3GBIL5SQCa/8IOW2TCYTeBgk4SKEESIXsiEk+aBSPC5ohJbZUrhKM+ y/a96Ltc3TyR2+01xNSTU0lJmsBr2qvDmqVzLqOmYNhLXlXVe9BgRxic0CCDdoaBoD4a1apy+uZV gtIlMPUQ1iyFrtHYgo3oTLWNvjOD1t8BCLRDyAMjvnPgtuBjvLZAxc0qq3t446cxHPSthuPe5cDs rfff+759ROt4lbVZCJa3w4AWOlD0BopnPgWsjkhPQQ+xIkWAZdhjTBQgpRTb8ZMAACAASURBVOL6 9Iynr1/x9X//N/LlbHVDDPRVyyxba3t337kGPIdeu+br7hi283XnuxXxfPXJzTP6oO5HZ2o+NeNN 5PtLTRE5F9VeBAygGY1Ib0HmW4yo+xq6o6aaXfKSrA3z+/KxDQdz1OJ9VGN2/3tHMDZQt4vbx5oI yb2oVXBYlgXnr9/x+PhJS7MHRq4WBmfn0v2cXL5tAogl32qmkWGq9Zk13akWdr4Pk6resz437Sac vnzG5dMjyuVFfbv8OXHBkVZgz/uBFdX48evDJqcPtjVeWy3mVit68/kwxrfo3KtarH5jp3evAAzy JwirPb49dN1kY8KbMXRwz5vR/2mbK20akWZWNsCv2l1LI9mABcFLvBNrOQVD6b2NYS93s7AennZs waq9LoKaMkAFhYCFrpaJWPlhbGBCM8i6H6HmyxLEv//j7zjsT6DwB16eLxBhiy6AlQn2A1whxZlF AEwlXGEE2BJVCQhZFFGTCErNyHPBkgvmVPCwj3jcR+zZfCEcVRkBKgAqA2GasCdG5QkFGWUpsPpy 5uQyrlyXPnW5PT8FNC2jh48WO64mocYYsC8EoYzFMmyWVEAZoFjBMQJW2ttjHnXfKDtwL/8uC1AD QCqpK4CwJIOWvIoQdhM4azRJKbUXy7JNOciFzZF11BQkq4waDGCoyYSbKQDEq5DRe4fqnsTRNrOp 0tx0s73klb/uyURbu99HVJ2jI9vY77G/9/rfPht75d+rGAdPs16Nb3iCI49OAZH5gxqiHwiF+2xA FDAGqI9FSTPOz8/47Z//wrfffke5nLEjzYaHUkHFpP3gRM8ghfXNTSIe4WT04O61JnD/votgNNIZ mxNs5dz6g7SjbrIAE2qWpilkFzS83x98t06D+QYQoSwJ6TqrJlJkvS/9yKE757/6HifU/ufm/vbT 6Au182LrAo8mqwaqrYgja+aacr3i+fff8YkZcb9XrYL0GiLud8FEcM8HNnARQgBb9Nbo66JzV7sA QxZmnhLEi+QRIEyoDEynIx5+/YLl6QkpJUCK7jWx1rbA4u5kfWylPqIVeO36GSDynkm1ay6lnfuP aEBGBr59jwuyyu9H+uUaBntHXzK4yCLO3FpMvPdLKQ+BWjrxdi8YIgVYhaiSRZL0yCOYMMqkDv1g 00o43RcGWbqEkf62cbpGy7+nri1jAEKWyI2cGDlt8v0pKFkLZ/pB8v2kZnxBnKaIT58/QfOk/4Hn 5xmlqGrfkyF6djk1hRSAZiDsQOSW5q66KUQAB8tqWVFJfTRKKsh5xnVeMB92eDxMOO12iOTqmwKC oIBQRIFKAjCXimtKSEWAGFWFmM1vwTdGC6qvRrul1RshcHPg00qdmkZcTKMwRYaUCCADSR0+c16A UMFTVoAxWe0VxrBY1Caz719DdUSWvtz8U4pKdgkZJISJtU+BJhAX5JRNJdrn2ZGrhlTawlV1gG1Z OYmQTeND7GFKFk658TfwTSRtG6wPZv8bHVxsEHhrZ3N/+3348EZbcufgvnoNmqJ7fVz15ZW2mvnG 7vHSKx5O6REt6nxXV9FBRFpOzE5qxydGG5hUWg1FkM5XfPv9D3z97Xe8PD2hLosmt7fU1ZpIy9Zy yxTbfOpPzwz5Lm2n19fvR6/XwIpINx2NDNmlMgHMDDkCUbIKnqYdGwDUD13k7WkETVoW5HmBiX0D iJSu99GFNlD0OpAhHwMwrPftvh4BatPi+CeWT8ABByoQGEDOeP7jG6b9Eac4IcbQa4iQgYuRaZFY 3h3TRpq5WJxReSekAw4xDlCWjDzllrSLmJBqxRQZx8+fcX58wPPLE6SoucoZyMrhdRhwBxn/A8gV H6QDrzz33ncy7oN33vGaluTV95CvvV+Dl5Stm4dwd236Njy121PaPX6gCF3z0pi+nyVvoIMEMTML QKap1zEwS2vD90wHUNaWE0P0XBlk46uSMNI/B7Z2zFz54ZNyc0+pFTGlgpQsEyRFCFnOcbHcB75g IKh5QaMcGIxgYapEli8BQX0zeDOpBGRRCSctglQXLEUwF8JxN2HHEzhMgIWcLQWYZy3L/nJJuF4z hLQ0OTNj2rOmu865Sf1EoS2o28tdtSyW1hzM0PoUFWLjA7TK454YTAFMFXMRFCmQWlByAdUIjlEd qZofgjNOoAhaGKHmRSi2bgQKQVW7pKmqHRgEYpV8icAx9pTHRlBd0AgGGioUoZbB+1hEkIx4UdH3 C6lzl9vrxloG7aAZsxt9KvxA6b7uoGA8cPcO3X3G/3OSydjA9vkfkZJGs4/X6HR/2+KSgjs+ebZX 7zhg0gGGz2FypjK0IEBNCefnM55+/4rvv/2By9MTUAr2TPY+zdTJRvjHglJtD9k7DA93s9gA6lZq cq/i2gWz7Wz4DNydq9cfNDBrv9204vvAKYuBaM2xYgSwChDQgDtcGHH/iy248hdslo/sRSIwJ9iK dLXS7s1m0qXJFTj6k0CrT0f3elm1aFKngBoYbUdKAMkVpcw4f3/C4fERu92EXHzaqUWoepsaZq0u 6a6JLGZqHuGMgjynqXbORZBTRogRcRdVayRa1Gw67HH4/AXx21cs5xdECqYZru351XD/IjzxV5pG /ky7DUA6k36nrRFwAuPMdxOFP7sWfNyM9bagowl7OrjrWgrlkb1PdXi/nzOYI676Vzh4EGf6DhrB XeNLGsJPlijImb6HgRN5rmuDNnZ+pRbb4qbp8lwaBKgPh3arbg4t+Tw14VWfj1+/vuByWXC9JqRZ wLxDmAiCjCJaSlxWGxMouYChqhs2VKUx5xWVNJFM4NDoZBUDGua4eCmCdC245BmnveC01wqDUity FpznGdclYV4qchGLrbeslwLsDpN5qkY1O5TaQuFUFeSEUvsEgdXKgPaDGVIJAi3oFQgarhgsYoWo +YmkvKBk9dDm3Q4c1Bej2iK5xkalzgp3sbT6ao3BS4hgLiglq5q3FKRiYYysXr1SlPgyqJmJjExb UjhHstq2b6aGtquCxOrl39HR6ugcRNyBx9ZssfWpaAdu0Ea85w/xlnTxlqbhrb/fIi7b70o7ZEAe 6l5Ucn8SFwj7eNys5POiCca0xkvwGjfmIHx+OWN+fsHLH99w/v6EcpkRs/n01AJhsbakSZ4NMzjs H7rs9Gl0znVit/YXkfUzt7PoN96dJ9k82KW0ziTJQO4KjMpwk+8PUlJYCL3KtO3hkfFLk7q1X+ul 6n+3fUU9uiEwg0rBfL5ASrYEeaPRYD0ZzpLf2p+rd23np2m7dN18vrqw5Opus2vbX65sEn0JLk/P uD4/Y3fYIdhnLW5j6Lx7j3hhqsABgdWRvana/eLeX30voWZ1RueoZbphoY8IEcdPn3B4/IS0XLsj bBULIVTauAVPzcH5zvy86p+wkfQ/qkn7URDylvbyzs1oZrxX2tpqq/z+tl9p9H3o7cr2HLTdOKyP Pbfuswu+aD9H83U3zUij8c0fi+ywkTTnS9Catq81HYCrar02V79P+ZSH0PZ59HsMzJj20ZNm+rPr +bdxwgGPzaioiTf+9tt3aPBCAGGn4VEMEGuSKSKtHSCWCItAYMvXTaWYB3IfaCRF3i7VuMRYjfXq NGvRrVQFS804J/NkrhWlZM3TX9Tvw1OdShWgVCx1xjXPOO732O8mhMgoqSK7dzYqIEEPE8EiWqRp MWCFYtR0oRPKhioDGMXURVyrHraiYTqlFJRUIIGx2x8AJoSoyUacIIqViR9rlrffmBAm9fDeTTvk ZVF1b87IOfVnq2DHjGi5GQC0SAUHBa5N8rn3OfWMaky9Uuy9Q+kbums1FCj1zRrsUFHfcMNhXJG5 7cEbpO6PXH9GrT+OcSRulWC5S8yT3saspECGXP4dTGleAiMsXC0bqIJ311RcLxek8xlf//U70vWK crmizrNm5JSmlbTD30hDk4zINSI3+bVdmtHfx88Hnfy9GfjpufO5WtmWG0EmbHvYniEFtZg0oqpI 1bwzVvPC9+D4jo90c2QUag6smJhRU0K+XMGlrvJleEnpbR97798c+c3YV7/D1w9Nq2RfDlK+qbbt Pv+CqkBSwvPXr5j2Oxw/PSJBsJTSJD/v5/gTMEGEA3Ita75IwDjatterFo7LgRFpUhBEBEyMcDqo c+f5GenlGdH8bXV23CmP2n+HV/2PXH+FluOtNt5rf/v9+4BK57+blnHzna+mOBgHBkHChQraPFcH mYCbRkJBwvAMESBVs8XaPZ7RVf+tzd/+sxKBPLS5vV9BDFGnUAwZUgDoWNwHbSVojBehCdPreVGf j5gW0kyb2EF8+5nkNVmnQWwl01XHxxzUidAnzja/1q1Qp8vCAqmqkqm2oasN0CXypQqWpSBk2+Zi qagF0Lz+loEMDGHTnoiqWi/zFTkn7HcRMUwINAEpI2eXVM00QlrhcyQ57BEmDZn2KWaqiEJaopwJ lARcBEkqskWVZJBm6awCiFYr9E1ENDhx2VizbdxAmsAJACbagWPErhSklDDPM1JakJeERQRTjNhZ bQutiDds+GIx1qzSUBFpm+DetdVEjAmz/OdKe4Gu0XCTytZ3gzabbqXxuA0g+aFrxfA2EtEIJsa8 E6uQWIvVDiE0Zrem1T1BFmDMQ2yHVHU2mogtR4HWBnn5/h3ffv8DL9+/I1+vli2xgqUigBGdaejC K+J3Baivj4NO8h71RHFrkjWynrfA2sfAhTRd3scu2dDANaA0tSiJ5qIJbMCCkGox4LthgsP7R+e5 Ng3+ngZgDTwDWC4zlvPFzI4Oop0hdsDrTUl7351xtX1+u+9Xv2OzZwZG0Zzg7D86Rg8d1M8Zgvnl BedvTzgcjgi70ObQifeoHXEpki1rZ84MSFljdxuXh0pDBFI1KRsFzTgcTBgpIK2Y+uULXp6+4+ly 1nTTpoKHh9YPIfY0/PwRyPpnQMJfBTAA3G3nre/ufd/ABK3vuaWPgGvQ2vyRabVkfdacHijz7WvQ zCD2vb9LeZ3jCT2IlQhN+zBqQOz7BkjG/pFFn/l+IeomTL+vnTcCxP0SdfyjvxWo80q//B3umIrV NwqEoiZMiqgtrl8grEmomAlxiu0FCQm1qIagFfHxCpkQULMrawVRIKKAkVtHLKyTtEa9axRW6X7J fBREVYjNZatROPVBSFXDsVIp2E0VIVgxMGbTeFgkixOKtoDVzDTertjEoiWvUa2Y+mLsKIIyg2oB VbUB15w1qiUXcFInU4pB++B1PdrugsW46CZgVn8RhmoOYowI0w5h2mGadkiLVpadc8aSk4Y6GtDw pFohqvONmH1fUyNLS/y1rtlxe7i8dshrpg4Zkr9sAUTbwHf++RXCDzCyO5+N2R3vaScUHKkGon9u +8X2kkrX0CqA1Mvc6xbRiCi01N2afyS6aQQE5IJlXnA9n/Hy9Izn799xfX5Bmq+IUMmaRfMwBOOS pvhWzZj4lhU76MBWq+PkqW3tzs3s2z+nmRgnuUm+I2gY1uy+RHa3KY1MqFpxeP9wwv7hCLCmO1fG 35nz25opk8DGe6jDhpoL5vMZ6XKxsF+NnqrU94cSRgB0fy/d9H/Tn3sAQ2wvaXc6M9HTNoAT6+8a DDICgLRkXL4/4Xw64vjLZwSQRYug0UJ9QsfkyZH8jKsg11t1tXdTBOnEoZaCkjJCzFobBxq2H4gx PTzg8OkR529/AJJhUpYBtNv5+lk2/2e0j3/V9deCDKAhP2CYGN+Z63MztjMKWbcvAlQdgAEUbPtU 0TaE940ap2/PNaF4YP6+Pz3pop8JKaWlXoABCNdakO0pF2K0aWrBCx61cm/++ucW8rqaU2htoFKy Vfu0tKNe3wNqOghTxMSkLtEpIM8zShXUVFArI4Z+7CIpQQ2iTm1EAiGraWGT6u8ho6h6mD3mtYMK wMpRD2ujqklbxBAw14p0XRBCQaDQiqaJjAfIf1Fk2OULy99BCjV6wqCiWoGqTCcw68HMBQQgFasy mitqKcglI+QIsfBViqFLGADcW7cCytRImY+5WChRmXYIIWJ33ONwOmKZF8yXM+bLFfN5AeOCGDTT 6X6/RwyhSeeBJ8BqVVQSiJmCfHOOkr5PR9u4WKPYLZK+a1bZHMbtpruX++K1Nu5dFWuNxUfsuWsN SjHzVACCgMPoS+K2bwXDBFKnWQEkaan6ZZlxPV/x/PSE89Mz5ssVNWewCPYD62STAn1Cm1rRpXN4 ts/B6OESOKGljydrpnvyC6oTob/wcmnZzW193aj9V4Zt0EfWPzPxGxkCjhGPXz5jfzoiM5AtJK5J 1r52jms20s/tG2Bzp/cu8xWXlxdlnlXrsVjTeqSHn2PTY6vvMRa/p2n1RPo4Ma5lW2WIuE3c5nQg UKr9UpPacrmoeeR4QDwdkIo7t950oKu4oeenWs2fQSTta7B5thR1PA+xIoagCQ0DMO122J9OmA57 lGWGOwz+NIr4N1xbcAe8vVYfae+1Nt7z2xi/J6xBwj3K42cd1H0mVm1sxuUCiH5mJSdWPvXi3L2F qI7gdgUuaJ0fw7VQ5I2SUR1nQx4uR1De5okYbbQFQzbRO3PnYbdbgUSfVlC9nccY94SaClCs/Lio PUZLDltxHLIiW6T1RQqAnJNW1zQnQkKPi1XipeTJC1B1dR50Ini9qG1JqM+x5edrsp2DEJHu36RO m0AuAobaNT35FarjTOo/22sdXBiBYLYp0rzxZPlvSIAYBJR10pkrgAIU9cyulseilqqOrjEgHg4g 6qp3D+1sh5p6tILUgdgwAAqIU0Q47LA77XE4z5jPF6TzBcs8Y84Fl+dnxBBbeta42yFOkzl2MRDd 4bRv7tFs4GXXx5L1fR3kblbptqmILOsg0FSDtaPxcXO9db0HLsb3bu99jfj0za6wu5YCzgYmXdVo 0q7mlmBl/kWQlguulyvSPOPy/QnLdcZ8vRioIExkiq069g3wzeqqTAZDigGGdnj1vhb9hfU8d3Bh d74LLP6cpPg28R4MDnYWG2m0I1xJQCHg8HDC46dHNYmUrFEOYmZHEXMkHE0U1Pque63/3T6zvjEI 1/MZ1+fnrjGEQPi10Uvr+wpwDG2/BzDaGFdEc0AxxhzE/GCa8NIEWmMEVbSseqm4Xi5YLlc8HA+Y QkSSgu0I/Cw544kcUDm0kuzMvTKvNMDqM0oaZZczag6QEIDA7SzvjkecTg94fnlRTesrKeNXH7/D 3P+Mr4Pfo6+5Ty8+ulZjGz/Tj3vvvv+drf1rtGgAGH7x8PnYnxFobLb/+ObhvYSbUFLjH6510M+5 9aXVzbOWxPZm40V2Pj1jK9rtA7hY9cucG4jaHm1PuBbFz/Xw37jbM4gFKWf1Kchi2mLuEwMtFiZE oBixZwbNjJKTNiRiJQCUI3NrX00nLAowyKJLhIo5VZqUaiy+DdMLCNSOR/pE6V1lpUplqC3LSLkl uSAooWOw5uqwOWtSihGS6m1bMpxAAEhaul4iVmBFDFDFhABQguQKan4gnXEXEcvIGVSTYalYQQ6Q BvesASl2G7VqU8I04fg54ng8ojzMmF/OSBc1m6SUNCvgyxkhRkz7HcKkkSxhvwcF7ilZOfToHWi8 vReXGm2/1UAH1b4zNXR3mGoa/Gb836A1IKA5Uf7I5QQWDooaXRjQ+3BtD3oz70DXuRVbI9dIaeY4 IlU5S9b8IiUlLNcZ18sV8+WiTrbXWevfQKtLBrKwSBObvcZLL5HtB9iAqu5GAxv+hUATQymKcPcL lew78O5S87uz9cpFr/yOlhyqM8v+RY8ekLvPd4Cv/iS70wmffv0V+9MJLyUjVdX2af0PZ8j+rDtJ 4kYivHtVPXuXywXX64xp2HOaKt9PbO+mLo1qK5t/xivv+Qhz0mad/tl/fc1WU9PvGVtisgrFS8Ll +QWHxxOm4141vui+Hy1JmXYMgEbJBNvzuj/Ecmvg5l2+nDUXlJwRp0kFJSWf2B0POH76hPPXP1CW dAO62pwQ4FVSSUbH1f8710dNLe8BHeA9MP3x9699du5H1WEE07S+Z73vNEy0gVOggw1yv6L+v9FX o9Fgp5Xwyqt+b73dqyr5Nw1G1751cNGEb0sb3sfkOSRdw913n/OwrpGxB0gQQzDmGQKWuWKuVauj wl4Cqx3iUhdp9ss4aepZiOWNqIRaM4oYOCHD/VZsKFYBwMies4EJhQmZVBKqQAuxEVArsuMEjzw9 qR8yqzTqDlVkybgI0HhdG2MxZu5SI+AJQ3SSmt8HAIg6z7D5n7iamjUFHygKIgsoCpAY4IwlZyWo 4txBUJasYY+ZwbEoyAhB/TLYokuImhamUQ0buyLHYmYTnespBoTDHrJk7K9XpKtqNOZ5xjxfgfna Fn8/7RCYEOMO0y4iTjv105i0D2xZ/SiQefjDGLCjDKjXewMXHfw4A9dQ2S301vXjUkfeskbr2DIw +97PRGPSfWcLLBLDD+odyO/OxQQP1tE9REV9iMjqyNRaMV+uKGnBcl2wXC/Is0btVHPm1Pwj5lwl RduT2rJs+nFseSlI2n7z+1hgceXez9oIuB9AH412t2P+98nqW8RyPPye9a3PmYbLdvDm73OAVMnv lwYKTG5BFUaFauY+/fp3nL78ihIickqW1dSYoqXR9Sq6/ibNVDsQM1n3wYk0Q0C5KNCrWUO1bVO4 wNBmYSCCbQ1udpi95w4juvlM4JrpQetCq/3Y0cyazI6+YSLq94AsmL8/Y3l4wMN+r74XA5IkEIL1 udogmK1yr2V4lXZv74PIMGcE1SBnzaLMbP5XzMC0Q3x4RHh4RL4uqKkiiNJTpzcOVojUFExjuvvV 7L7PrD/CyGnz809fmzXcrv1HtRhDc68qb5qfD4Z7BhpO7T8D3SPqe4kGE5sBx5HJt72mbE23iW3s 8XUAmhuGkGX7bMoAbn5J3iZAvQjaAIy7S0AHGKAKVF6dIsHoXzcMcgVP/F7d3xFECAxEiohGVIUE KYumpnakZloBZ+jEAdFU5JWKMnGoKrRQJwQQQpBqGgHBhIoMQhbS3OcEFHNO1ERRbmPyg0PWXT8C ZqppHKgdDfuzH9yBrLeFBFR1zbByx23B3bZPqgFpuSsc9BDAAmJB0ExVQEiQxEBSE1E1VTBTQKmC UrUIWc3qUBOmCIqTVjQMBoRsJ7sErOlMRTUdlvI12RLSLoKniP1xh13K2F0fsL9eMZ/PWK4zlmUB UkaaM4pUZGIsUQvPUND38y4i7CcgBtAUQVMEomb1hJmv2PJfwGaraVZarRQntvq5WgqqomERxDhI WSKdmXoSKEjzRvZUzxUCWMG2agdp9ANphxE95NO/Y9vsZAeFSoUwaRbTeUG6aJr1dLliXmaUlFBz NmKsReQYpCWuCSCpPV2yfebp7EfG1Y5XO626f0L77nbsynjWhxFU2w63I/PO9d4NLU5F/9+5ZSOI 3ucWZ0/9ve6R7tJKFfsXAkqMePjyNzz8+h/gwxHnPENz6zJQqqa4RzXnRdXUNB8oMxKpntHeL1XP 0sDkIhh5mZFyQiUTEGBEsGlGOsR0wtjzhBiFuMMh3pOI25lsQNh7TO3v1mqTCaw/vl1JbeaRBBFA uSyYv7/g8OkR4RA1oaDtORYFUx12q4kNQWvYlCIQKSo4DYm0Gg0ccZHRmhCDalRjRA0MPp2w//wF 89Mzas4Iw/4mdq0Vad2Ftim6U+t6doaBvzaH783xT2gS3ru2Ld4DGB99/43jYuM14z3ooIJef38z mwz9dE1syx0l/YwSwYQsNi2DNeb5L0bNiL/H0ZDRxUruW7jpPw0g2QGCdqgBY2kgpa7G4EB267w/ ztLYKxHd+21jcwg4HAIQKi7XhOuSmwMiEEFCrWpnM8u4VC+wWgJVcwZINYc5VWX6oRUocQjCjeCI VLB4umVFXqtDAxi4aWKYryyamlUGqEfbgQ5jF61lTwZ4mBlkSZKKiNVV8YRcCiqakt+JNAFMQZ1X LelSSgkpJxQr9tb6YYCjFnW8ypzAk0aYBFUbNcpI1RivI22GhryK5m5w6YYI4N2EwzSp2vPhhOV8 UYl8SaiXGTUnlKxhrrIsAOn6hsCQYA49UZ11ycroagU8xhR2umaWmlgJnbKEajVbqvdb0RcaQ5Ou yFMiVU3DUQeAqqYUryvnmxFwsIXuJynohNB2u6dIF1FJrVZLTmYRICVp5tZ5VsBVUtLCWpZThKAA Qqo6ZRJpnHd7r/++3oH9bzcDaYdvCY+sn1unCx6vzlLGF/xZm7ar2h3EdSwjnU6NbNJuIBubMry+ /4sANTDC8YDd4wNOf/sF4bBHJiBjGL5rtuzfdj7adqlVqw9DNGOqAAjUmDoTIS9J62fA0mbDzJUd na3OuYMPH/ePXO/a+NG2og9zszekf25kzIUwJgWl8/msmTunX8A2Vif/Tk+JuYtRRC19fzHTa5eY 6ca84T5UOWdwCfAU5cwB0+GAw+MjLocj0uWq4oGm9O2j68PAsFI/NI8fvX5Uk/Cjbf7Vvf6ID8iP mGbWjqNDG3AeqWvXTOa2FGPiqw4oBl8OH7/ULkQ4ILkxy1AH4GRnxhETqbDQt8PAd1v48u14x641 cEGW2U0fq4hBcNgRmCNSrhodUROqqF+Dg4LmXALSDC1VnUEZ2YqLVZsUfanb+JXAmYqWFFiEqqGv FVZUys0i0mU6amXfYWhrCDYVwD04XM0DtLlqS+mTWGpPpU2BEWJskrvOoy+UrDbIuElaKXQvPLQw UslIWZmW+x60KA1Yiu9aEUpBsBwWq2qnxMg1NybgajMfv9JWZc4CyygZA+Juh8PjI6QULOcLlusV y+WKeZ4hKUOq5SFOBTJ7fyqyjStz0IxsTAhxp7VJoo0r6j8KERTYCrkpQEEgkD1LlgSmNKDvOg42 M4NybxIabIrSTUPi+1dawiSxaqJ1AG0pmyNxragpG7DTdPClFpRFE761irJGvAOAAGkh0wTzBeoy sDrYmgNulygcbK4PFTnXpmGPrI6brA70zTXaBTbXzxJgas0awyMMDT03SAAAIABJREFUWonGHo19 dBuqf81As70LqxYzMwO7CcdfPuHTP/6Ow5dPqIGx2HxXy2/jKN59irZEtKlxLVW4E073A6kimFg1 H9lAYctUK9KKN21npY2KNh/8wLWe71Ea0b45w3/tWaAz/dGcojdUzNcL8P07pk8n8HHfwsbJxt1p i50c7rll/CxsJeRNL1tRwzAFTPudakcICDHidHrA5XhC+f6ktKCLxG2+xM/iGmn8W66P+FT86P5f +WC90cb7Z2s7s7fPvPW3DOfvrT68erV2bmnK+NStSc/3Kbc7t8BidKIdgceqn6OGw1rqvP6tbq/7 HUV6aXC17WUwAYc9Y5oYcyrAnFEWTUtbEQAKcItsc0jr7vhNW1HN21lrGVVUV7UYwaZKVlRHiVqt hKLen5byW9qEjaiqq5mofd7NGq6m7zPT8qiTT4C5vZRqpdYLQq2ar4KDLc4ALlZTupaNiBkxWNEX ZoSSQUnrh1ApPSKjQ1Q1HYloynImtSmHAJomLVnbnD/7JoK9tSPNTnCYGTQRwjSBAOwfH5CWGctl wfX8gnyZka6zhhCnZHkaBCSsuRoEIKhGSkgwc2raDThDJWiyHiZwmEAsII6gAASeQAFgihAmyM4A CLwUMFt6NlNxiXsXE2B+J+Lx3aKaCAgagJDSf4qImn98Xu07d+AUu8/DLXV8amNmIQjMdwIOFtGd K6HwtKU8GkCpM2S/zz+9OWDDHVsCckNQBJu99fb1IYIswGskoEU5WD97wHBnhp6PgzmiEDBXgewC jr9+xukff8Pu10+oMWDOGXNOyLW0xGN6/O3MDoRxZZWVLk0JYE6xnggOiFMALQVpXjS6wfYItee3 ZK+1vJLefuZqhHbDHNqZ73LN6r3bvoioaXQElyVnzJcLruczToedanmL0UO3RgySEJH6XYyai0YL e4ed8rf+l6KAetrvQFDnahZgd9jj+PiA828RNc1oln8xkGljVnL+7wUWH73+jEPme2181GF0FCh0 f4xtjPd4m+vTt2Xsr733RsNwR9PRHYxv6QoN/AIeEr75fjvu5hy61WwQtT0wOjV7bZJXr83QIkSl +Gq6VE38qIhXzT5By6lzVUfFbIkpQerECQwZik0K5GhMJUCq2rarmIZjiLkjAdhNJ9AaImBGDQSi gAD39LeIjpUE4STSR9tVz1K7fZfQwQYPehvyCAqLJ8+poBZBpozJnFW33rHK0GtTKflicXNyjebf 4FJzQkq5Mz/TlnBgBRhWplpAINYIBtUS7NQnwxxP2catxbDMH8NHTJ52vrM+Dgwc9ph2e8TTAXVO SNcLlpcryvWKcl1AOUOKpXDHyJRUpV0EoDLWOJCmpctyUaZMChYYwUClmk3KxJqvhxiqWVI3NhK2 XltGQ2P2ZKsP8TWuTQJsAMI0PiJqyup55Stc4+HMsoqY0xo1/w1V8xtBqNJAcGd4On4f463kRk3R 4OEzr52zrRnkHkFRAvI2gftp7YWD0SGG3v0eHCzSqvdr51ItlEfIINAhYv/rL3j8r79j+uURORKy FMw5ocioWq9NE+KxHOQSOQhedIwBjWYwjRxZmGUz28WA9JyQZi2xTjA6IdLAJ5woUh16TQ1wj1Th R68tIde5MxW1drTPMxQod2ZswEDlIxV4qp5XFkLJCefnJxw/PWLaTUjUTaAVsPLt3KCemymprpnS CrjJeqcKBCllTKYZBRQwxxixf3gA7yYsV10TzXSw2YNOIn9y/v5fvX4cqDh9X+PV8SiPZoB7n71m khive6BnxWfugAGTjO73WgQgbokQx8/f0uLIdhD2jr6zB0fTD14RFVaLovh0QggoqEouLHFTYSAh YakJKRV1+hDvgDN6Y1Ke58JiUiXA8jo4c/EO+wFtb1YvcwGIzdGLAoqpBgs6WFd0BajqXQsJ+Zc9 /tz6ZXPV3AlrNROEcRIirfFh6c1rrStzx4jw9NVrFFh9IYnAgRBJnfr0+axJbizNt0C0+mExVbL5 WdSimT+xEHgqIMveyV6J1aqjtrGLlwsXY/TDWIwBs5ku4rRDPO5xfCyQlJAuV5TrjOVyQbrOSCkr kzZJikGYfD0cEDapVpOu6URUqE9FMXBgIXCL+YiIO5JopVgWNsa8yhxzs7mbK5FLcdXs+LakYfTc g3jyiI6yyeCosDq2AQjNjOc8vUdtrM+c7+vNSRkPuX809kFGFj0+ditl/Azp/qikBehZG/0cILSO ljIQ4FEX7kHe4HqYkASQ/YTHf/wHHv7r7+BPJxRP850yclHT0yqKh6ibPp0RW4a/Hp4OkIWswgB8 hWgCud0OALSGy3xt5rGWOFd6+fJxDrfL9fNybm/RpdC1JmMrTVJTqLaekBcv7FzfNbMlV1yfz5hf zniYviCGgJRTa7cOWUgD9SJ62QsRbnHAKCwNoLHWgpSWpvkAdI13xwOm0wnL+RnFyiSMGenJOvuj Piv/7utHQfZbvf8ZwP4WY/brRjPwAWAxPrs26RhYNv7a37cBGFj/bLKPCAbLSOvPdmJahtDhc2lj McHe6GoD0Gscff+yrR+vz2fsTkcECxEFMbLV1ahgFCGkCiylYskFc6lYSgVxAVFo0ooTWmoSqW1R 1lLnVBlSivIWI7DciOCacIsklGJmGApmqgjNH8IzW6JJmdy1IjK24z4bfogMxBAsH0V3Ygsxgix0 ruQOCJhZU3S/AjJGnwwdFbXv/JlpmiAimq7cIksgGcUG4poYMYm6zAngDAnZfB08hJSbqaJFy9jc i20yASy3gzvkmOTuDpv7iMPpCEkLpnmxAmoJab5imWfUeUFYCkI1PwffR8173PM8WNvUR+7+L1Qt JbZLyoOkL+N60+BWOByiUt3o5v2nNmYFo7U9QyDz9/CWehIZX3ufGTeddRuj98r7NHCK1UGUJhGv PvV9ILffv2kS+QG18w8T1oHxruiAdEgXjenDcs3oynVQnpmwe3zE6T9+wfHvvyqwmCIyFEjnUlCL pcOHa5EIxEAptREXMmDhyspRClytexXs9jvs93uk8xmX8wvSvGi+C/jaQekLe2K/YdAjMNKF//B8 vTGTsFc2AjwaSNuZXRFmsTGzatkqVIXhYygF5Trj+vSM0+Mjpv1O6QGqnk2bL5Wuumf+dv1VhhjV UrJabKU1CbFMDVwUAcJuj+PnTzh/+4paMlid25Q+/AUz9u+83jsHN+fNfv6cz8X7fdm2vWqz0Re6 +9xHrvaoC7BDe65Ndh8ff8B5IkxTuCFhKxo7ftYBhVFysVgxj1B5Ba69NYcEIP7zf/03Pv/tVzx8 ekTc7VBJsEiBIKBUxpwqzvOCy1JwXTLmVJGrSj3Mljp5gO/+3z5QIwaWGItIw8kgRZmpSFPPeahf lKohUkJIpaj/BWsYJ3OwxFcuJVFjdq5SZ+70uzuF2qDFJCdnCCaBMBOmMLXJKaUgZZXQcymYpqkV EfOJXRGWLcCwv8eCX15LJKWEwIxkuRWk1r4h0dW7JamTIoKGirYkWcEcKFsVSnd6c/+FMbhNCVv1 wQOIkcBxh3jcIVZBLBlhnkHXK+Q6o347g5eCmrSWTGMKht+YbcMZppCWVtgOnUcAoLtK6lWN+BUD CmFlgnAGwQDEfCRGpq9YxaNOVEJ01TTsZ+1KeTQzhwMQDH5C6OCCbLnuOVDp2m4/kxUDa2v/LvH4 ue9/SGsBKEHQjQ4YqEIdfI+AzpyJUEmQCRBm1MjYPTzg4T//gU//+A/IYcJsUXFVBEtKQMrgqgX9 yBP2ECxKrCN/9T/u5qRRpUuBmyYxThGH/R4xRpyvMy7nF9SSEXyP+Qu20iH6OVwDxWEyho9o89XH Z7Q/OUbZ6HulAxDoHFCoFlLvp7lrCSQXS4Z3QZgmdWKva4dVHyNRjxipLRPyeiTruWiUt1dZDlpU sULNLNPpCNrtUK9X0yar/9U4t/+3E2i9dv1ZULBt673rPbPC9rPus9N/ru7HfYF/5RPh99wAysFQ N/Ca1XgMBJOdxfXzdzD3SJxH0x61sIvhJVsejyb0+bftRXbF3/77Xzifr/j17/+Bz1++gA97ZI5Y iuAyz3i5KLBIFRafzep8ZY5CQYDIXfXpjHHIz91UpMQeelmBAlSUVga4dalWMDRxkQaLFpXwq9r/ JUxarRJmznHJHc7kfMDDZMFAhqnXHfVRk4b9ef0rTjsQF4CyahtyQamCkMuNqcTBRltgiNHB4bAO q+qFicpQETWbpsSdPxlDYTEvzlWqEYuAaNoMT87lo2xRMsRr4skWTmvAqpCWSmEQEAQSJ4Qp4HA6 IOQKelyAecH1esVyvWpYYNHcEFIKHBMTNGwR5FEhzqhbubi2BKO6sFbNQ7HVBDVwQUBLzk4Cz8PO w1oCrvmSvt4Dw1xfugll8/V2jbwfvHncGVcbgyKRlc7D23uTaL2rsVjv2Z+9iu9BAB7vzibNaGIw tMggIUamihIDptMJh8cTHv/+D+w/f4Kc9igGJEsVlFwgSavBxkpNpQ4iFPSoh+rSsGktGkawoQmk ZfOMIeBwOGA/7VFywsvzM+bLpZnB1Dm8s1T3G7nLaEZG6/xymPKVp/wb89eZq7+jg1X91AGFA4x1 a1XcTGog34GC7aF8veL8/QnT8YgQg0aHOS33vtmcuQY054z2UuDtfWZCU7XyDAih+bbE/QHhcMD8 8gwpvjgVLsGOqfffuj7qQ/DRdj763I8A7T973QMPr/XnxiyyAcDASJ6GsW7ApAOL7bto89OfkTv3 6F7ahL9vDkMzh9DwDICWfIhk2PH3x+z7s7e/egVimjOW5Svm84zL3y44fPkV9XjApVZcrgsuS0YS AiiiMivDN6asNTWKZt4jLSneRFzpDNvfqEKLZ8cEAFMfOvMRDTvkShApINEi8DsOYACpJuSqNn6K Ezio41mtTti0fT2EzuyoESUh1RKUUsBuCyayXBJo0QaBwyrM1Jl/Sgkppfb5NE0NYHSJYzPDw2Le gIwQEGI0h9IOMmDO4RpoC2OMAskK6pa0qE9GiOApgFmJR3NCDbDKos4wlSAzKUOBSPONa331UNgg mHZ7SCngnHFYMnJKSNcr8nVGTjPS5QpIAbKgwrOhFpBYzhKbTxl2raNcItLEkTTu6n6PCre+sR2Q +AH20zAQ+xWQlBsHt7eu8ZC0DSq3348fygAsxu9vCc+Ws909ph/s6ccvAcwkpuvM0HTSREH3UaV+ fkGozOD9DrvHRzz87Rccf/0F4eGEGgMSo4EGGYAFVxm0U331GtNFT3RG1X9Be2+pBaAAioxpv8fh eMAUGPPLjMvzC6RUBDjA3M6RA6ctsHAwNQCLzbONlr67SdbrfWOW2N7def7qe9OTtbUgKMgrS8b5 +zMefvmCaXpQeuh+KPbqSqZpGoSYVsPntV4P57lWrXVUYlH6QAyKhHg8qvbiW9SoKjK/JCXihqx/ 4BDd9KHT+j93/bmzQa/87i2/CS5/ovdvaVVeBSBvNmg/ba87qPbr7p5sG1G12Os+bJ+h9vmqPUUM /Z3Dva+Njej+mCKJhoy+fHvBfM2I36+gz59Q4gQQo3IAQdP+emINIRrCSzXdNxPU8ZLML2CVm7zj LiJB4P53dY2HCADNac6kFVU1INTr2JNmcJSMktUZUmqFcAA4KLABm2aiVyxpdktjXCDSGiNFWn0N DzwlUvW5q/xF1DFymqYGMvxfi/4ot9oMQZ/scWP5Ao5x7SOImapWWU3LAqmCXDVttfs+YJDAa85I uUJmIAQGx6hRLsGdJqGhsQRbP2MGxhSIsFKHNkZBhJkEiAQKO/Bhh30BduWEmjJqSsjnq2ZQvCxI 88WqiRKo2JyJAQRSp1LVa9SmMmeywnI0poceJFP/ewBkza8HnbHcZ9fbcLHXD7LYS7s0PECClcap HyA1pQ1wRkTHMR4wq3jYOJ6190oP/tKLADAHi+jyqsJuV+UG7IoQJESEhxMef/mM469fsHt8AA47 5KhO1GK5RbysN0pBFGpaSx//yt9JfH3c5AhjkGg+HcIEjow4TdgfD9jtJqBUzJcLlvMZKE4YuxNv W/EGMAeJ0Jlqk846oL6Zb6KxwQ9dr4KHjSahq7at3wRIM99RB10CzOcLLi8XTA8nhMBwd/pRnS4+ d55My0OJX9Ua+PkCpFQUySghI3AExYgCgHY77E4n8DRB5oRW88gFMvJpumWEoznxI46KP3KtmfM9 UPnx601gMWit3vThwO26vne9p815a758Pm/MKSskdOv/sOrZak3eNuPcF4b8RgJGn5431mLrHjC2 HQWWAbIWXC8JNX9HviTE0wmHhweEvYZXCsjSPwOu7nOMqgXaBWwOiQwCyFAxqJnEAbe/DnKH1Qrx EtMkBGaAazDCqEzKmT+LIIsVWssZEoJpMSbNpe9sWGjoYT+osOQ0QkVBQunJtLRgkB82S8hFPcmV i0bF8i1osbesmo4YEWNACBGje5Qv6JZprhaYoJ7dIYBiRJwCSqkIDchYGvGWM4PAlgVVGYjmGUhF i0dxVCfaGF2r0b31lUFSNw3YWjT1NYkCNhQIGFVsHeOEwBFhN+FwelBtxmVGSuoUmnNGscRHmBOk ZpQCkBRoEi10TZFJ/xBZa+9sPGj5Nd4mAPdUll2NPdy3/WRFMP37XkwM0qvFdmQ+AMZRttlIE06k /eeNhP2Ba0tffvRhKRo95YyjkHt+a5ZVniaEaadmkC+fcfzyCeHhiBIYyfY1WIF6TUULXtWiae8d rJDPhWlCVOpoAFZxgIMAE0hIUxMjBPBuwu5wwLRTv4N0XnB5eka6LiDPMFid3xlw4XGNN8MGvBNv Ts/WTPnWPA5P3ZH8jOo3oEKrr3Sq3Jm8h8c6HSsl4/z0HadfPyMc98hpaUKObMZHphF+l6Fbl1oE nmsvJvdDgmbgPRwQpklz0YjSODF44xmAt4q2/wlzxNoX6q9r8zWG+rZJ5hZU3dWY3b1umXr75g3t xgpg3BuDdAf7G2jQQHO/76PA6N6+Eimv9tk1ZPeAxdhWLLWqTwNY49srkC9XlFxRU8H0UBAPR0ic VjUAgC6JuJDm9SFYivpXsNuRqJNjAYqli1ZVbUUxxqPInpT5sUCrm+qTgUTDPEEoVf9eiiCVpMw3 ZCBEMAcgmh+CM0zcwcIcECJpXZQxOoQYh6iOneRhXPY/DeucEKKl8vbnagXXglw09DTwtMq8ub3c jKJSX/c4d4tSnCI4VITIiCU0DYm/L6cektcSIdWKYtVOKav2qAYGs/pmaIpzbirWcQF97skIojKF YAzVpTG2ZBGaIItiwG5/wARLO1wWpDlBUgLOV9R5xjIvSMuMkitYKqiqqtsdgD0DZks1XQoKgE4K P3Ztpat7arq79Q42WhP/2WqgNICxfhfRWn5YNb2RjH/OCY1+mMD2d7imSP2RsoFxYdK089MOx89f cHh8xOHTI+LhAAmMTKQGrhAUEFaBpIwyL5BUrHgYUKpGUNUhG7+qQ6x4klK1DizE8+ZWC3klhN2E 6bDH/uGAGCNkKbheLrg8Pavp5R3JdZRvdezuU4LNN/bXm0zklWulMdP+iLARz9rOTKeH3Q+jw1X/ qdlW2L2GqtZZupxfcJ0veHw8ggvbvN0ZgWkviGr357gzHwBWc0Airey9nlvTxO4PmPYHLOFZ8yHY O9QMO2YIGrvw12oq3r7+2vf88Bl8wzfqXuTE3bZf8Yr9qb14r2u0bms0R1XpvbznNzL+fU/wvS1X cB8sdb611hV1cCGAsCZ5qaKOg4EZ6XpFWhaEOWH3kBBOD+DDARwjsh0X1WJ0JbVXyaxVXBFvB79v VzGHsjE19BiUz2Ark24Z/zRWRH0xoPUFIqt/R2CAc8VSEkrJkBBRw07p+xCl0RxfbA5cGg8xND8F DxGtJWOumosiWhKacREdGDRzC1HTKLjJJHBtppIYY7vPr9LS7xoSpi5RCWBJwMS0GSrPVIteKTmD KRvgUJNJk0qgYKOUDCkESdB03iGCouXNCAziMCBgkzKhy+A+GWJrCwottFPXuiJ7+m4OmqkTwISA sD+BagE9ZtS8YDcvWjhsWVAW/b0mNfmgwjKYimYMhafiHndxP8yjZ/J2k683/I3I1WjVKIF4+3dk gPvtvPLZvet9R7C/lniuHcQIbPlGciXNtBojptMB+8dP2D2ccPr0GfFwQNjtUImRqnvOkCW2U7Nb vS6QZQGDEEnNfgWqfagmTauGQrr2y81uGJZLRDVqIYCmgP3xiP3xgP3hAC4Vy3LB5fkF15eLJdWj 1sB2ybbzu3qPSdyvze7PAL3R9DFqMFZttXsGR0+Mp9K6Z/NSSwFNjJwyXl5ecPzyBSEEy0wrrb0t ndn6d20vxzUu7BGTZbl17w9AiBH3O+xPJ9Q4AXPu9/ak5HY0NgD9LiP66wHHasp/6vn70v+P3P+j 7wN+HjDctgfQBuCs+089ieBKkDKhbQsM771gbJf6EzfzdG+Nt1qO8e1NuwtEy6tnFUlV1c4QsFRI YeTLC1LO4CVhenhAPB1B086EFYJwR76DckLxetXWuTGPjRTv3tRoRYjBYATS6qgMdUL0oDX3p3Am OpG0omOpVuSSkC05FU2TVvokav3xQ0TkviPQ9NtRw7VCrZBcUAxouMljDEGt1UpqDyGprvVwkFFL UZsnqzOkOkp2vwxl3HqInRBsF8sruaopQU0m0eZwt9shZ80q6v4fIg40dG5aCG4BsnrealGjwOBs ZqDg4MqqoOrebFVgGzhzAFPHQ2SmMmPQ7huDyJAYAInYnY7YSYXkipIW5OuMuiQFGEtCnq+oy4Ja LGxOWFX6Nj1rxiL9twGl930uQ182l9/UnqP250D6++y3RaHNT3/PRwjqvef959vPNzOA92k4V+Nd gAtIA5AxTSHFCTtzloyPJ+weT4gPD4jHPSRECDMWsPodMatkWyvqkoCSUFOGLBksai6UUrRII1PT Lmk31FQJ2Pm0jLsr6QgKLHivPjyHh6NGZBEhLQsu5zPOz89Il0v3f/JRic/XLbO4leBN47Ne2H/b 9TYTlOG/QBMmRIWbQBEMUlB1PuPw8LDKQnyjeXPthUAlSwPeqzDfvkVtbsgcO7P6c5lvUJx22B+P WKYJBRcAMqSCd8z9CoCh8Sz8+673/BM+8vyPMPs/CzC8DaDTl4/ce+9y5ecbT3ca9oHn3hvb9pu1 sPKBtb5zDwGI1TdqO5Ck9lVS58tSC9L1gpTUph7nK/aPn8C7SZ2E3Pu8MXFqXs4iAKo6fQbiNhNe WMWZfmMVRlOFQrPjCnmAKFl1RCViDtyiZa6MREjIWErFxVIHh0nDVpum2up/eD4IcxFRYBEiAgsQ IjIximkhSlGTxGRJqNjFKbL8GqR5N4RJwzRrBaXaIigzgMKk0R3TpKppjs0PpYfc2Rw0oa3vFPWt 0EUPQ+bQaUKLftGQ2YScC5hj89FgUdAI8xMptSIvalN37UwvoBbR7Lu2GN2VVIZQY1PNhr5u1cbi awYi1QyRgHcBkxwgD+obIilBcrHiVDPyXJCXK3LKqoq/XkC1KHitGvrqR0nD+WpTg/sxoE1f20Su NBoulQ0AwT8fJGGdb80k2iWI/tPPzGuX0nnPRNp/erC2Gdn0XS5tU5ds2aq1evCtny9NdKX992ob 4rY0L3QVJ8TTI+LxhP3jCdPpBN7vQLsICWoy9Pwwfu6CFQQsOSMvql1C1boezNwywSoYV2ftMUjN bfzNzEfDySZSgL7bYToewfsdpt0eIQSUvCBZUqnL9++qJSkFDSBKf0OfrIbL+9r7nIu5cVvBxHsE 9WPA8Ibcrn72Fnw/bZ6Qrr3RflqYZ8OKunenCvx/7L3pduw4ji76gaSkCNs7u+859/0f765TXdWV lXs7BkkkcH8AIClFhO09Znaf5lp2TBooDsCHOZ9mrJ9PeD4+g4k030jXT2ciIQQ1FUNUK7llK+2/ 41K0EOJQCqRkxGi+azEgHQ+QYUAJAVGi7rNgIKZbj4/HpwkaPdi/N3qP2rdqPT4KBL4ZYMhWnPC2 w/FvXendAx/54H1EG7QF7rvbAru18fh+H73HR8/f9ztVdby0A5T52qJGwABCKSvKuWCer+B5wfjp E+LTESElcFTnsWKoHKT2/TZLAiarDyHmoElWm6KOizMBdfwiC6frCWmo+RTcKm/mElEtRozqkLmu uaa1jtOAkIZKFDU/dbAEMlvG7nbUYQqglEDLjLwsmJcFeVmRUsIwJAwpqS3TTqaokhkHIGQC5azp yplr/wWstRgyYRgOWonVGDz5vY1Buh3UF/reMdXTjUfNMa6+GUNEytE0LlK1KSIC8r4q1VIAwFnT qeeM4loMq3qahqGmhvU5qgu22wB7HusFwQCARJ0JPWpBANUkmdNqIMJo66EY0MhrBkrG+uULkFfw UpDXWUuoF0s2JsrkiFW7Q6xrYS+sagrsW3TfqH4PRHhznrCdTw4DHCxQXY1ew8fJSHuVRpiq7ZLr tW0I4c7CFWQL6joIZGm1fdTFQYVmzVXHSAKCjWUaEIYBx+MR6XgEfXoBHY9IhwkUNUqALZdB8AyS IpbTTEBcwLmAlwWyLiglqyIyUI1iQGyqeR9G2A4l9pFUYCF13EjX+ThifDpgOBwRxgFpSEpj1oyy zLh++YL59RWJC6jkeo06ZRU92Bh14MMl6QYem+bqPSJ9TzvQVgbdENH6uysJutO3phrXKLgpArXG jRjYQGGEQkiSkb9cQP8rI40JWdgy9dINcw+BwLbwegAtMF/XHoyhZV1lYXAuGFIEmZNxnCZgGJR2 C7SuEZp/iQLX7f23/dkCDP+G7ozrR00pzbzwPuh4aBp6h3G+x7w3fb13/hugoV8//BUA4d79PwyK 5MEHn5Odefbe9T8C8m792h73r/pcbGz9HUFzx77K2MSSJzFj/vKKnDPSMiM9PSEeDnDPdN2UVn9C NJ8DgVAsdFQd6ywXQm9eEKN3PQom98yIdejIqlqKRUh4/QHE6vM5AAAgAElEQVRfCiEQppjAJWNZ 1dkzDAxKEYiCIBq6Kr2vgvtniPpMK8gIiNOoBc7M12FeFlznKw6Hg0r6MWhhIWphpToOAZwLclam 50mAZM1q/5wZNESMg2p/YBoVP27j8LlHjNS8xve+IJ5FNDGqE+j+rwdU7MBPzIRlNQzWedUwViu3 HmMEgofzUh0rWDVd70MlnEbxnN6zaTK8/wC68GMAIWIYRgykxhb+9AlSMtgkadVwLAoyuIDnVVPJ 54KSVyuMV+paCKxggNihXbAMnY0gNtmCsSEjwppsSjxTKKokB/bPAUJ8/1XYMpjaweiBmb/4GGrB NmF3qNXxWVEgMdR+SdB8FAhJwdkwYEgRcZowTEekacQwqQ9DmibMKaLEBHZtBwDXghQBUgWHukYk m1mvaDVaT7PvmggQ1e/aTkOz+/rQ2TFFFOinQU0zaZowTBPSNCJNI4QE18sFkjOW8wXnz69AyRol UqmP04BOhrwjGjdQUZfWD2uPnOHkjWP0yx2fN02ndHsFotV3pQiW8wXL+YppeFETlJQbTXPdOyqV 2VqzcfL7VXppX3YSsZpqCyjpNcKQEA8TEANKZkTyNfs+o/lZ7Uf4cHwtoPyW678FDMg19t/Z3gJP 7z7jG799VOPzI8ZxAy5adHmTAcSkQnGoTgLhjPl0wrKuGJYV43PW8KZpQhwiMqDMX3QgPHWwQAwY VCRhfhloTMs2oW4Q8icxTYZPbFDp0upiuWOomLo3EmGIGtmSmVGuCzgS4lAgMoBiMjVj23xZuCaZ EhH1rqeEFBOIJ5R1RY5XrMuK19OpJtFSs4LW/QhEWhV0JPAQEEpEyRmyaqrzKIQUggKbpaBkRg6L mihSQho0nLVOfycpVqdURSs3UlM7hZBShIhWQuzzcbh/hoMM6UIu+1YMZOR1VZ+U0ECGh8x6i2iA wcFGDdv1aQaqatg3h+df8BYcNBFQDgCg5eNHZgwsEC6AZRxcr7OCjFXBRVlX5JIhuYAKayhsyZAs ECm6Do1wimiBNpBLYqHnVaayFjNXmVnEaz4ITKNHphW58wrSNWlEZi/h+rM2CO2RIaZRCIQlRXDU sGatkqsaszgcEFPSvTYOiNMBaRx0PgyslxgRQ6qgsToFWmipSNNqKQDO4Fw22jI3k7AlbaJ4h8FK y5wJUelYoI6eIQSkccTh6YjxMCENE8JgGpYQsMxX5OsV5TLj9MdnXM4nUOE6Dk2g2Y7bPanRwYWf +177CHN467u6X+5co4EPB7C6AMQ0egrsdb5ZxLKzC67XK86nE6bfXpBi1CrVPvadBqV37NTr7/e+ nyFV80im4imWQydENcmmlDBNEy5m9gqmVpH7j3bznKg9+Gu3H+FL8TXX1LH5/vs9MkN8GBx1fKPn IfvrvXfv72mpai38wkCLW7e8+b5VCFrvgQLAuaCUK7gw8rxgPBxx/KQOn2FISKpXhQijVM9Wq4Uh Sni9CmlAqImktGBap4Kh5rpibqHaxxDgxVDhGhfbkCFoVUG/FxGQC4NZpd8wDEjjiDiq+p+5EUYi qtoTNkmAIiGFETFFpKkgpIRlnnE6nUEEHA4HHKZJQz7NQZRIIMEJsfpjCLQ+gGaw1Kiawqp41kgT BReuEYnBEmx5LhB7Pk/U58WLqoduXRRGmAgGNDRl+DAo2HCQQSUj5wL3QalrTjr0WgChAuSmXYmW R0N9PyzcNiiTfpRDsIKj0uKnA1waJhSvUSEAm9MtEWlhXHsmr456eHrW6inmvCsdsyQuwFogXiQu F1urmr6cc0ZZVUrWqBVu+UPsL1LP2KSur/sPducrT94hUn0iXMVcNTtBfXaE1I9GNUWqmQhPI2Qc kZKaEMKQENOAOE7qH2PaLgQ1kay+h8kis0SqZq7XFjlgKFYBuFiel1ogrEZYOQHo5q4jTJ7e2/+x sNZQJtVUjoMWxzs8HRCGASEmhGEAQMh5wXLRxGvXz19w/tdn8LKASlHH0HB3SGtTreKW8H6Ed7yl qn9wRnfsjiBXxKgIWt/1xHzX576vdZlJXf8lZ1wvF3ApSMOIlYpq+7yAXwcq/M8FDWkdAtBFsRA2 +W24FBSOmqsEotWux0nBhlzhQPdHAIbvYejUE6Ef1H4WwAA+pgn4nva15hMxQt7BkY4/tO/eAkA/ AlsQAYlFGpiAMQflyU5X2wmAxU4XRJM8ecnIywqeF2BdMawL6PmIeDggxoDsxYyoR9qehKsxRS/e RXCfC72jwgmFFr6RQ5X4IkChemBLCCBmhGB1J6CMIlBAEMFaNKKkmAQshUFDsiJg7hzXhl9Eqq00 ABqZEiOeUsIwTpjTBfPlgvP5jOU6Y5omDNNY66VEIqSYgEk1GHld1bZdaw+o+QVGvDUF+KpOqFGZ d4oRQ/Rw1gYmqHOQbVpjY8NdAhSgJ0qaMTNGjQwIHG9MJjW0VaiZc0QZOVseiryqJBysvkkKmso5 Gnik0FS4zgwcsLn2AtA11jZHG/uujpuBS1f/NtOMgIGkQc6UoPNt5hAR6Br12grSlbS3DKhg0VDY opI7s6VAZoHkbNlgO9NUB+Ac5PWtl+Zc0lf/CRuP0CWCiyrB+3qKprmKKULiAJ5GwLLCIrZEdGQa itlS4gZScFFNmQaKojm8woBCr71yENZrsIIYsPC1ZRO1V8H6+11iYbjpJA0J4zRhGgccntREw7px gKimwmW+oiwLKCu4uL6+gjJDcrb1gC3RedA2Tm2+Vh5Qxa8BFk6rbr9v9/TxEbKFKtt1bb0yikXd ePa/6j8ipanL9Yp1nnE8TIgUUMAbv5P6zK65qEykEWnXLkubejhoUNNIo8VEhDgMiMOA7Fq2es7P a98tkX/HffcS/HvtoyaEjx5777zvuffb13+cKv5jj/+99xeNFvGb9R7kDMCLfNXfjchKEQthFMtL X8CcMXPBmheEdUV6WjEdDqCYEKMm9WERlB5FmmOnaB0zYwRQmyKpZN5oTUtWA2es5L3W66umRe3y FCMSAorlgwgiGIgQKWItDJ4XLGtBGDXrZBySghRqC9GlNIGr9i1iggjj8YBhHDEdDpgNXJzPF9D5 gnEc1N48johWf4SiloyXQbDMWa9pzoiONP35cs6QohqONQRkC4f1UNbYmSX2UiUZmLrns+HvPbkX ITZp1kJpVbLVctHFKrY6wwkOOqEAs3BByYRCAYiERAESCMMwmh9JDzBswYZOvb3rH0xlHNiZc78q XR3sE68AlcXlRgEogskKsyGAzLQSBEi2kIkFR5izHKu5iiyUVzNC6vjD5qdpNWysXRLour31qZC2 fm1OYA7P7vcRLPS5gj7XGFgOkpVhhd1CXRNi9XNysTRHpsL29Rmo+ZT4q57DNeLJk7C146T2s5dw utnQ/ekgo2Nq/rghBERSh+ZpmtSpNAWkcVA/pwAUEDJnlHXBOs8o84Llyysuf3wGX2fAoqwkNnv1 lk33f1up66cIjrL3+fDxeVuDsbsIPKpG/CEqwGadW2P0MQYNyX094fDybLVgqGo4esbYsLibWnwC O92aaF/R9dcFOBZR+gBCHBLSOGCtjqKu7XWK+9+rfS2A+RqNxxZkfC9Q+raRr8/XCXLtas38ujn2 wf2/b1/ptVPZSGHqhy2dL0IPLhxqh2hMl9XxqDq/rSsWKeCcES5nrE9PeHp+wXA8QpJLPUbEjNA6 Yq702qRkIk2S5c6KbTA60wqgEjac6REQNfRUw0QBcNGNWIo56jEQAjIs9I4zOK/gYUAYRmO8JimG nfROhCJqnywAKBLG4xHTOGG+zpgvZyyXK86fX5FixDoMGC1ZkIKDZKMYkUvGmk1TUJm2jbVtcM+X IWTZQw0UeMG0SOHO4u/g2BsqNR9Xl05TSkaAFDzus4I6k60Sso+JKANTFxudk2KhriEEIAbEGGro K9xR0fFlY231f6LUpMNKJP0oqWnvg9b0rk8sooyMI7WwP9HaNwSd9xAtKFQE4NAlfLLoEhIM5jOi xN3Bt9Te3kjC/r/GF1tfxSMEPJ256uBcW+g978E9OhCy3XeuURBLSkdqHpMGyEMwpC6CUsz0UTVS BpRcQ0jUavzUVPKoTE3BpNEDe0LXPkV3wg4B4zRhHEeLpFJAHbSgjTqTmglwWWas16vWprlc8c// 8zdc/viMUASS2XygVH3vlTiaqOMaK2mATXv0rhT2rRIxda+9Sey+BsP2MHrpuK3xtkLc+0iz3TIK CgMhDljWFdfzCZIz4qDRVLwHLb6mO7X3pi/eH8Bsp0GzJJtjvq8BInWypxhV+LE8JtUk1vX5Z7Xv kcy/VgPxPe1rTSpV8/U9zHnDc7ftrhnk3nFOwPqLbkT1t25/3wj88XE3cNHUgN02YpdStvHUFYET 1VLgjBZaKsSQIih5xjprwiTKatsOhxE0DEgUkDXzRX0IZVg1Q0YdFA+5rJoEhCo89amS3PUJgNLv mDQpExgUVG0vMSKvBZIFKZiZhIpmJ5zVXBFyQRoGpDRoFAha+BgI1UEOriEwTQdFQjqOSEPC8ekJ r//6A+v1ivPlgst1xtNxwfGgmo4QAsYYkGJCBGHhjFU8iZheX0vFOSNTBpelOYOVnNVpzhxT+6Jp Pm73FsZ+cexVlESkmgXP0ikWdcKtNDwzq/apLkKT6HxHiMbWgzRcUkJAMRNAcfOASevJ1PxEngHC FxpvTFRENZgTAmAtxTRbhrBtjagmA1qfpiuWBzHnTGgonrAnKfOrou0B8tIYBhac4Pr6JEHJArXB 6PcVbW00B/qb+Dg7QCGB+w257byZB1VTFyxape09p1ZsJsACYqqViAVQs9WawWWFiJk9XOvUmTd6 rZer1/ekRFWqZCDHv6sIryaFG4aEw+GIaRo1T0x0vynPQguABYUz1nnFOi+gZcHr77/j9K8/wMuC xPqcIfRsvNMc9GvYieOmu01K2/OA+0Rwx7DvNrr51FPBvQaDIBCps15pWqNS1J9dtRgtw7Fqz8q8 oMwz0pAqmN48R79f4cuz7WmH6XUcbT+a36hGqnVCQkgKLkBK5/0JZENd3xun27H8qDnhWwHGz/Ch +JH3u7e6HpmD3rrnveNvtIt3fntkzvya1lOee9d5+4oOLjrIq2jHfuyIc90s9rM7IUqw0CdpGglC wACgFAFfZpwyY55njM9HHF4+adrhoOYKdsJITtQtb0VV+XUe66SOjV7Oe7thUfsKmAkjAioKmZMn BXVmIoKY+htQO30WT6W9YF0zZCiIFgkirtqPVFMiNYc2ZybKkMOoER//NgyYr1dcX0+4ni94/fKK y+sZh3HC4XDAMCUM44BDjAgBQMnI4AqSpHCT3hyAGsjwsvBEAZlyK90eWypzMv6xUYneWWAPkaiY Iyuh+lVoeuKW5nxvTvHPBAUQfh227KCllJoESjloQLEMqjG4P4hGS7BFPigIMT+cjonUPAO0BQdi k59yBy6MCjdiXa3gJtF15Fh8vXfRHN3F6zEEeNTI9uYKgMUdJGFrp1ufddP6g3TrluqDoZ3fTYrP U7K0zlrZVzOzluJ1bnLLaeHmL2rZYWKMN8yKAulY2Oea20RQ7f4emhxjxHQ8VC3FOI517flxgFQf jpwz1nkGcgaWFZfPX/DHP/4TWFdEIRCXWsyObe/XBQ83qTWTGBG1TLGhjSl166FDqdg3vfwtuNbf +vHumLbTo831umMNXPtxnpenLheXEhyAiqAmaYsaVh91sDCfzhienm+yGYv0Gq7uyfpxqbjL6EbX U13+KugxM0CiSQFTMrNXHze1HZ89A+udLr9HefAtZgrvz6/QWty794ePf+ca7wGM/hnf8uu4168f MTZ3azH1v3/gGqkxbzcz2J8RlRY5YduLWqa5CgzQOXmJZRgEgVmwzjMu64plnpGXjOnlBePTk/og xKSOXpV5+FWo9Uncd0BNMEFUYn/YBBb1oSpBCIGi7rpgUq5KUz1oYQ1ZZdHwvDJDcgFSAsaImAat x9FtWEjzH4khVEbFIkAMmF6eMB2POF4uOP3xBZcvr/h8esXr+YRp0vj/4/MThmnEUxqwCiOXAgZX U0kfUua72ImHL6B1XbGuq/bDqrKmIWwYS583w8e1PcYt8NiEC9s9QyDL4AkMQ9qAi71TqFShlWu4 vRcpI6gpSMSACQTFmEcgNZuIeuFuir8p02rStK9BdGsRYp76DM2ZIA3MkCcfcpCgq1kjgsQZgh5H Lhbuxro192/oOlBlVPf/kJovY8PrfA3Wo3ti0PpVc2EANYTUtQzOIMrassgyZ3hosWelJVA1HTVH zU4qpb7v7rsklqcDTcINofoXTdOENA41s2uMUfu1Z9QIAGvhM54X0LJiPZ3xx3/8HfPppH5ZFv0D jxSCVI1UqGPZ5GlnZrQZ1DZum08PiOP3EN6tnF4hzZ3jWtg1al8cBDWQ4TibAQ3jXjOWy1VNQ0NU h11p2qe7PerTlW5AjB3hH03RVh+fSNGZ/3XA90e2Xw0C/jKtExJ8Prb+Om+Py41h4iPc/NsO/mkt bSbfNwH7YjUvYzSCVwUuI8D6uUF0TXtdKrNIAFAY5XLFOWfM1ysOn37D+PSE6ekJFEgLoZkJhCio J78xxl5iFila7py3PgNNAtf8BWLmRhKy8Dy1RyvR0giCyCplsTm9USkgaIVYYQavK9aSgRLAAyMM yRzUCCFaHoCOqFY1PgEUzTcjBYwvz4jTiPH5iPPrCcvlitM647Rccb5e8HQ84ulwVJs1ERipC0XD RmIPQa3t3BGbCr6gxxZmZHMGdYlyw6SJqjZI+dUtmnYp7dby5tKkMYCgoagiLZ+GMr2myciuHvdy 6mjrA2TRFLAxR4EUoJCuMddkxBBbemsipBgraKjx/P4MgEqB0sn9xrgc/DVtnWvdHHRoqyne+23R tbekDoFVlXRg4QTfryzey+56HTABQbUPNv9iyee4MJhL+2zfiTmjbrzDRdrzi88ZbYBFlYwcuLoE LG1eQgjq9GfJsKbDhGEc695DUN8r9pDy0Ir6acjvClkz5Lpg/nLC+T//hfPvn8HzgiCeZMxBU50o y8TbJGn3tdgOWsfc9wkxfjBD2zhU4g0NRv/thpHU4a3rQdOUG3iytcJZMwuXnBFGjSjamDiprdOb 0GarGyJ+I+9Zz+QUMcLxjUYuKV3w8ut1DfwUqPH17W3t0l+0yRaq0YM18vh0n6Db7z/8/Pv0qvi1 QI+INBQVsGeRur78X1VkmNzQrblefeFjYXJGcHU4Wwy/OlKui+YdKLlguVyR5xnT0zPiYcQQrIiS FAUY1Daxq23djlyKHhOtpLgyHSgYgj8D1eeob0OoE0eBTC3LIIt9pcAKHOw+WRhlyVjXgjhoVEkY ErSEmGdwVBV1sUl3r31hQSbW/h0nHMcB6fmIZZ4xn0+4fHnF6XzF9XLFeXjF0zjhMB20BkNKkABI QH1eNgpF5rQnDuY2YNjNOwSignXNlRGklGoBNgcH/TxKm/ia8nm7kMXACG/OJXPYiyGoFw0LkFDV 9fqqIKNP6uTXc+biEpYAiOLSXDHQVAALFRbSRGX2EDYmoQIM9RsRwPxz3DThXr9up5bNk+1ehdu+ 7sDrWwRXun20yerZjWNTULgUTqjaCFu4QkDOi863+zVxMU//FlYLuA+OP7enaUe3H/sZprZlqyak /e7CABEhWAXdYRoxTZOFySaLgjEAJIyyaKVeyWXjaCwiWOYFZVFzSH694I+//QOv//wnMK9IcG0M 11Dv2k8BQJ3pqgLHHhi1p3o0B9/ftgBiDzB6QLa/Y28+a5qX7e/VoZfZIqgEVFgTwq0rRnqyOkht L1aztWsnes1WNY/0XSNLBmedEXWW18UWajg5VTOmU/HH6/xroye+pz0CFh+59l8JhPxqHxG763ec +/19FeFOc+Faiw3V9eiA0NTJpOYO1RgYU+hQEhEsZtFsqNyyISa7frnOuFh0xeHlgud/+w2HpyeE GFFs03hVUGauUnfNlGiZYQprzoggvWTuhZWa8ycAsx1TK8VOUFDBparxQySQBJSiqcXBBciCtWSw FIgUhDJYGl2rxVGdH6USdhHLRGrPkTkrQDqOmA4DxqcJ4/GA6+dXzK9nXC8z5vMVh3jCMI0Yno+q eh6GlnZZjHHDfT+kFnLrEYYSllC1EgoGGDkXxJgrE/BIji3IcDLeu0/6LahKTpv7VTWvXoPMITSY c6774iSPVoDWEulNKsTtXtGe0G3MYuugZzqlFKODNo/UabCIsNBaGWH96+Y9UKjaN4FfozEOruDa wVt7hT3VzWaq/yySyPp8z7GqmfqoaiEURCjoYM7VYVrcbNDNUPBV4H3rHKGZOqCAndmFOmBRr0dV yxWDgvVhUqfmwcwfFKOCPNeYEGoytnVdEUCYTKMBoJnJlhXr6YLX//wdr//4J5bXExKa4yBLUU1P 1WAZIa49b0uzflOHc0s8985m30ce7xHmZh67DzapfSNKb9wU57/6mvWl5HDeM8JCGHldsCwzRkjV ttbrdufod90arfuS+p7UF000T9v5p2Z63Eam9O+/vv0VTCFv9eFXMPr3TM8/tw/yvRsA9zQfX3d/ aeDCWC90GTb4q0C5Q9rG5Mj9GUDwAgaaXVMdPrl616NmQgwhmLFcN0C5LrjyZ3BeUeYF4/MRYZrA IcCUy0rUpGUQdL+JqhoWVQ8H0ep+gQQkqYILAM1cAlTpl2oMohFmorrJGepjQKxmGqEMtuJOYs5z ISWkQRDMVOISbrWNWyw5u5TRz3YgPH16wcvTs9ZW+OMLzl9ecbnOOJ9PCPMV4zhWNXSt7VFNL00a 8oXbGGmLsqlTLbI5rljYGaV413xSZUSTqqsg5jdtPzf0ZgxAAoELozF7VW6nYPZjwNKSi6n4dW2U olEpwgxwi/Fv/e/Wrs0Z12MEWohO/WwYpRPbnZh3phOfKwJ6AOK/sT8sdcd3kv+9vbSBYiLtzz9C Kibpc2cAuj/aOAuioWLy8bcbB2xBkM9L00BYwq3dtTWiiyoorL44aMwlRkuGZom9hGA+QAAV0yCZ QzFDNpleUxpqxJKHL+dlweWkppAv//k7ynlGZEEAg8jzkzTjm/dH96E9Y/B1bqY4F8D3jK/7KKYF 82veTNUHpMh70OGjTWnM/evVeQptLio7N9pR1hXLdVahilqoOIw2395vuzfcb6ZfrH2YtUeL2E0V mAd1rIY0v5u7I/R9mOPDrdei7r//EUz5V2oSHoGcNwHYnl/sznvPoVVZ5O58+Zqp+z54rjRJ3KHT mBKATU5ta1VJ3Gs5XLXWfU0MI+5ifgG8lSqlSaeaxElQlgWXdUW+zhjPR8TnZwy/fUIYR01/XQGO 9rOvLug1NkRE1e6lgKChnmLFqhrK70mZfhH0n96Hg4bziYduWZ0EIgykzu7s9u0san7IudZ+iCmp g6oRcZf9SQQglePVEa8gwH0iAg4xIE0jpt+ecTmdML9esJ4vWJYZyzwjWAGoYdRseiEGpDQAUaMs hHoAY43QoiM3//W53AEUJQPUSa4OMixPQaBohNDWgWlnfHX0e4DI00LTBpQ2jt71hTSHiVUmqXPo DqEobYkxlwZERNdBCBGAoMhOKrA1Fkk98Nt69ZvYR0KVnx0019wdRMgwAGqn+hr0ki5VUNxcWuqX 4v4lgiZV+o3hfWs4rcNrAMyq08F96u5Ito6UgFjiOAP9bu4RZ8a25QJpkqtgOUaiJfVyv4pguTWc uYs0JsT1uurAXKTXtHCtsTMMAwBgsUJzpy9f8Md//B2nf/yO9csJkYsWTGOnHVJz3biW07e0m3iC DdJGFhfB3seiDu87EvOHJepOHvjIGeLzjB6Y3O676lBZ/dZ10dVoFPOjycuqc9xpFu/a4XtQ68CA jAH1B3UgtO0XfTJyZ2kie+77TKWn39/TPsrYHx33o7Qi33ud957hz9be3C0s+8E+9bltvq3pWk1c 3yvbEKJuJTo9vIU9OvnOrnVhs4ESiqHWgWBhBGP0bZELIBq2qCXTGbmckecZOJ0wLTOm52ccjk+I g4aDGrbQB5c2Tq7aYxZwzpCSAV4QIysRjY26G98yQmDE2oiXp2wOoo5ppZSqjXCbZC6llo3nUpBL 0ZoIuUBSQUxsqbtjVYgIoAW3jNqrGlrDARfW0uxhGnGcRoxPTygvM5bPr5on4/WEeZ4xX68KMFJC iFHD/6aEYZgQkjIOo9EowhpxAapSK2BsvKpDrXMMjU4pjJJNm0HBaqQIYhgQ7fquhXA0Wf0KbC3A CZoIYhfKWKV1J2bAxpSkDFjvG4bWLzUzEUQKuEBfGRDoZ4ARhSyBV9PMOBB1TYMyV+uLgyRuPgu+ HqsPm6Uh7f0mBPrK9jmIpag3LRd148HY0X8ysNCp0r1vDytLi1QYTZbN0TpnoMKuY6XJlan4ehYD vFqrhIxZq33dDCruo2T7Mrp+oNv6AKpang1UrLwLOyZSX55B09Ovy4L1csX8+TNOf/8HXv/j75hf zwilqP8IlM6IVT+NvXYIjbHVPYl+jG5pELo11a7SDnqLQb35va2X+xDG2TnVtbs/rKcv/ZkOQgTS gjxE1w+5WpAFZVlBa9Z9uOmLm6mpXqNWrqbu/ujvT5ZVqPPZ8P1CAAeASVONC2m+DR3KDp44rd2N 7732Ufb1VqOdINL68ecy7O9tX+Uz0n/o1qLP7Xvw7O4ddvv7K8/+eLPOpZp6xf0ZnGGgbaJ7Dj4i 5mi3O04EINbkPnqceUZD8wewgxEhEMXNcZw1tO6SM9bXE/jTJzz99gnDYYKkgCJkKlWYhOXSgUlj 1qdSVhRZETggihYDQ02lbGdUE0Y3mOQx/gYEPAkRsyJ8y1aZc24VIVlt5Gsu4Jg14900grhFZ/Sz 6sRa7xd8r+sth4QpJhyHAWVZcTgeNEfG5QxZC1aT7JfLBWnUOg7DNGI8HDBMAxACVpBWpbX7iqm0 EUh9G4xpu7QUfY7M+VJLgKt0mMGa3yNq1ABZMqxgUsCuq5cAACAASURBVOdGzhEoYyPVAAHdZsKG Vm0iJiod7rBydbol852xfAZK91TqrpqsWjfDoyYEHjnRm4P2pqH9Yu7vHUI0jRNQU05J6ysRqtGu Gu/sdzVdxM3l2zN+pClo0L1ku1DYQIJUAODLqpmfImJQEBiG2Dk721r3Od+rVD1nhDP0QFWwdhBd XDNo2qNgav0YI8aUEElr51zPF7z+/i+c/uPvuPzzd/DrK1LN/KhO0tH77VKyeDH4lg03iIMvn6dH dRIauGhKsq3U9bVq6RpBQ+3z7XIRExRCJwxSpaGVXtJ910i/HPm6L1KLAjIAXhbIuiIcJwXirLrj zEpJFdCS1oQRja5iE/SCD4QD4wAEU+K5RpZcEAiCQowSRDN5dsXulLbusjebKfkRKN4ywPvNnbmb wCe7327f74/72e2RVuJ7+/DVmrPuvD3YeEtzItD1cHuMR4h9sBMfbLe+SNpSpyGDo91KaOn2Ifub C/q03J0kaChZBdpGvMTVojA0bIPmgxdEiTmvGWs54bRmlGXB8bcXpOMBNIwIgcxcYVUhrbKoE8ZA ERJUxb6WjFwySkka7REjksWO96CiqZFr8Juq7i3hUB8S5sTZv+tVyEXcu181GBqhEeF1JeodewZn Ukj11RAgjRFxTHg6aITJ9HrGfD5jvVyRl7VWMuVcsFxnXE8XjO7ZP00YoxUSSxE5RGQvrR5gpgTA hOc2PzCmYQRfGbFqC0rWrKCeTdOTmaWUkDz6pAI1M1zZpdo66k1UDcxJ//lm0W6ZigamkIW/GiMK BJGAlG6dpnwuemm7X8fSzZfnkiAAte5Dd0xlCO3i7XOnMg7d+69pHTs0cEGK32wthi6xGAArtrfN ZVLBLMH2gifQauAH4lJz58ODzowlsKgq1dRpiLDtdiJzVBQMFDCmAVOIoFIwny/4/I//xO9/+w+s v/8OOV8svBvmO6LPVkyz1aJANhC1jemGJt17/2AcfyAT6tfAR9Tg5E7j4hqK7XX6qCEyRCRoNAUA hARcMnJeMQIWJsptrXZO3DWHDLnueHtjfVszhNgcQ+mVHUuhAf9gWto2hk1TUffAhxjTf7/2Z2lN vue+/br7Wf1/67pJ8/P3YiWgS/FWhbO5UEdEt9/754C3V+ItMwCcOAMognJd8JozzuczpucnPP/2 b5ienzTPAVwjUk9spCeo6YFYKzGuVo0Ug4eSUmN4vrPEUnlbr3sHv/7Vibh7xOecN5PHrDZTYi0F ziXVdMmh1qzo0GMngbqkU0iQAVBISOkZL8cDjvMnzKczltMZl9MJkovZZ2dArrgSmRPoAdN00PoE 44hhSBiHCIka6ruKpYU2yZBZS5mDbezJPcqN6RvDUXCisfmFAQ4ECRlMAZrywKp0RgINqVa27bUX /RhV5kJ0n7m8tXK6ddPXnbm3mfav/v7Rn/bvFoDcu8a99x/ZyL0D6f57AEi0XXcOHmQXAbPP4rhf q0ADddvbGejotYv+jPbsbNoG0YvAIYBLvpECJgpIDJy/fMbf//Y3/PM//oHLly/A5YxowEIvCgPU 7TqofapwXntmKhn91PKx6EGPQOifQ/jfbQbi731xTyPirRRGXjMg0gCjmzJuLwrA9pp0AINc3AMc xauJr2V11RB/VDBy/8r7/v/69mfP739VYHHPDPNIy/Ct93h0T8A1F/5j/a+AoyPhX9WxTkbHdsk2 AnNv2Nwrn2AaDBbkJWNdMnjJ4FUl9cPLC+I0IqQBIEvyJQXuakjR1dqioY1FczGUdQWvKyar2inR 82MoAauFmboe96DCGUeMsQILADUFdn1q8Uyf6ssQU9RaIilaZtKOKfRSguGq1SVEqJU9jgPGYcB4 mMAvzzi8PuH8+QsuryctMU/q45LnBbysWONF052PA4bjhOn5CcPxgGFMSGlQDQupFoC5WDl1RjVr GchhdgAprmKoYIMzgyVjBbQqZggYQoQkBSwSts6CW4anKdz9Pm4qcrXtvvXr7i3mvV+Xj5j4/rq3 m6StV/1p+6rJrB5v/PdoQjOX3YIIACDLWLn/ra8lYT+Y9CubZbQnLD7O7bOx7U4abpIzoUAgMdR6 H1UJJWoiHGLCIQ1ILHj9z3/iP/6//4N//uPvWC4zZF0Rre8kYv4wXMNN/f7Vp4Jqt+3vA/bkzQA/ EH7eOPdbnPG25729pogMnN/xVfPTXcN073etTKwp9vv6Lnv2734+9Tv3u3FAaVqUOrguJJhTrdh+ Fm7Vj1Wr8fYz/k/7fsDxMx1Cq57wJ4Kit8yKTqOTL1gHFh7Ktg1xukVA+rU/RPeVna+RGtt03hUm e/ZA6le+9w4gQ+kBwBACIqkG4vr5C+bLBes8Y3p5xvT8DErJstdWURlEpOm9DQgMSf0AyqKpssFQ p9OkdTOqitg3e9+dnVTaZwX1P68c6iBD0/Z20rBtaC4FFAKGUYsFhRiqs6irpBmCQurgCBIUWD0R UA35ex4GTIcDnp5fkK9XzJcZ+XpFycr0yrKiQJDXFfN8xfl0QppGDIcJYRowHg6gFBBCQrKKpZw6 h0gjMC7BNwbUxgEm/biWg9jrnhDKmjfpu2OXwpug0TnVcmLptr1Qm37LdxfvPXD7UQS+n8d7r7fN gcz+9/jOxt2t6Qd9efwctyDJTtycX81p7cCu9k6TW92rweeXu3XJd67hoZLVWZu12F8M6mcQWVDm GZfXM/7+f/6G3//2d/C6YqBgYc7bLKd1a9p37bN9Z3RBAcwbw3pXE7Ad04en/khC+2BqRSree6iV +NDFTThRU0RnpjBtnzuVd2olO3OLVvqfFcRvH8EBo1Rn27fbR475ke3P1lj8Vfpwr73br3cm6lcA j+QSWQUZQpUItdvv1JP+IVAVbPvz9RRuHvi9OnMDRNq/9rBmEzaGSqKx8TCnJOEV5/wH5ssF4+mC 6fkJ4/MBwzip9zOrrwDIVPVoRDWkiCGQZq4sDIhmjvQsnyHFjRp/33oG4MzSwzg9oRCvGS1SwaQ+ kQo+AKtqGiPSkBBSUpBhhMFTR6OIhRxagiEirMwQLkhDxJBeMD09oSwrLqczLpZavCwL8jKrUUoY kjUxz/V0UtPMOGjujEk1IeNhQhxHhFFLwlMMYMtv4AH7zKwgh7nNtwT1LBepRa9WEaAwKADCQEEB ExlYan/JHQ87ANKSXFkCNVsXbY1sZuLOcmpguD/8LZDyra3XMtzX5r29s9lDtB4wx1tntvbqwN2R sLlR17EDPBwRFqHRDYqooxcz7++uwIQ0T4lqmhrgTURIpolAYXz512ec/viMyx+fcX19Bc8LohAC GKXkjomJAY3QmFmX48FNYxXkiUtdm1ntXp2hPp6/e5qJj2o0vH27yvj72a/SC6CUrJlYQ8C++w7M 6nDY2IjAfIe6iwFafFG2o0YuxJkW415a+j+r/dkM/Vfc/yOm0+9pv2Qmpe1PvSdtbpw2m7k+cJOm VcXB9fseRVdJarNspTIlGMC2wP12m6p4M5ZaBTPdARQAcKcONIe7hASJhLxmrCVjuS6YL2c8LZ9w /PSCYTpoDoQQ7Qn0CpkVbFAkUEqIURM9FS7gtSAQq3MirJYJmvPmZix3jISAmj43uF9FXLHMGSR2 lYqZzEvEgEYIysTjUDTHhKXmjhRAGRtVNpmyWEPHLBokAEQBIU44TCOGT8+4ns+YTycj+DNyXhFY zRbEAOUCyQXrdUZOEcsQEaYBcRqRDhOGw8GKUo1q1iC0Z0OszmTFwIRH08A+MzPAgijcUleXLSMj IjCpOWlf98TrVUiQTYZNr9K5WWP7b2S3yPHziMQjf4vuiDfP/xjduHOQmxXq6nZtm1TmUYCaXE4y txDgLkU+W9bcqrED1eRyhuBbVV7o+kFhLJcLLl++4I9//BOnP/6FfJmRoBKKlAxhaAZOalqVEELd /iBNEw9RUETU04EGksQliTqa2/Gmne/F987zL2dmRpR9hvdmLC/qJ+zjIBUcmi6pgW4LN2eIFuAT iwZx51K7hxfoc/gjnmk19/fp1Ry/aCz+p91tHzHfPWrvagB/ZJPth96It8nQqR3bRurfOoYYQgYq sbu534YwmAQNU/ULIKR5CJpEZgGq9pnRZU+0vgQ7Dtkc3kQ1AOup4DVn5OsVT8/PSIcnHJ5eNLkU NYc08RA7ElBKQChQr0mr5rloYSitv9EiT/wpfO9tJEWXBkhzQQwWQRGwIJeM7JVCu9hxtzszM9Z1 RSl6z1IU4MQYENkSWQVl6B6FIgEIMWmhNY9oEAtjixOmIWlp+09PWM9nnF9fkS8zOBdTUxeAGYkG cC4oWQGaxDNoSIjToAm9jk+IKWFImoI8xqjZPNGFM0LA0X0zdGw8NC7kbE6imh/BCSCM+Llvi5SC wgUw7QwImrwqUfXT6E1ROoQKAP39XQAYaLPIe4dFP++tdu/YR+d/CwG4t2fuPccjs80+csn/imVA zSUjs6awpxAsRLWZIQRt7HoQC/s9iu43zxS6zjNOX77g9V9/4PLlM5bzBbwumgxPxJiTApK6vgk1 WsV1gZq4S5PX9wqVbao7bIHFu0DudlwfObPdG9t+HD/aXCH71jWbEHanj66t7bh4E1z08ZvJkW/P B7p8RBtZzjFj/SxErTQ9ix3rhLb5W7Rry5vo182EfsS9sfsabLJ3vv4Rjob/Xdr3jMeefvyMMX5v z2yqonoZ67ADHN3l7KL2iR9dnLrNcl/Dob+HbQdrWme2Bb4l6CStEmmExvCXIijnCy7zgvV0xnA4 ovy2YjweEKdJS2TaPuTgYZ8aex/HAYMk5DWjLCvykjW1d1B/jJQ6b+26m3YD232mQIgh4fgUUbI6 fC7rosm33GEKsGqsVoTK0ihTKSjrihQjpjhCOAAx2jWD+mKIV/UkzTBCzi5sQ6eEmALiFHH89ITD b59w/fIFy+kCmReUqzq0avpmJSTBpMySZ5TrjCUGnOMrwpAwDSOGaUSy1zgOSDGBrTJsihESYLH3 jalHGgDLIhkrqLDU0V3qa3GgIRqiyJ6qumrM5Ma/xcFFDzp68KG+OrcZ5vYbrK7Ne6v3HfDwaFN9 dNO+f1wzS/T99rXfqgQbqLD1VWyM9VUB6DAOSJGq1qI9RHujpjcDFgQMIMhakOcL5usVn798xh+/ /475fPZUtZpszlTqgaBhyqadg81BddCm7pmlrbmm0DSm9g6x2mss3mofASJfCxLbOTY/pprs1+bm eFv7t9feft6YWy1hmjJ9M41SQHV0FR+vJpy5BstDmDdaCAcy2u269wIRwFxLGqgCjPGxFbx7mh3A eHSNR4Dv3t7879i+hbE/Aq1vXWcPJG7A3wf78Rade/Rb37/kTLyjNXdPaAujP+ixtHXTQQEQWqy1 dmDfaX3tl7irD3ukHOwiYsllAqB+AlfGZVmxXDTD59On3zA8HxAOI3pH1RBitTsTEYZhQIL5Gghj XmZQiVWr4GmxfVD78Efvk/9WhBFDxDAGpCFhGEfkkrEsS9VUVMWmEV0xZ8gCoIQAjnqNOCTQkFTj Qxo25jVbyMfftDj2o5duQRwTDknrk5R/0/Tq1y8nXF9PWK+LOV9q5r4ErWhaippNChWU64KZLpgt uiWNI9I4IKYB4/GAYM8WBvMZIaBWIiVY3fO2RkgIESoNu5bKVefOLINLaja3zjw3ZacBdcrtxn3v aFuCZxRtjK3XgrWFt5XZ9+8eSbePAEdbE7cahw1fl+15fV9EAKLeWVY24+AAjUvRkOJesifU2h8h tbTcHgLt2nXXLsAASLCcGBABWRjk+Y8v+OOfv+N0fsW6LFo+nRnRGFCw83sVv5A5clKTbR3T3Gev O41Fe5IH7x8TvG+RzD6iQbp7zkbu+TmStmstAnS+6lzXpfuGE6bvLbSSdxFo2WbtsBqVUtgSB7Ro nu9qVVu1HeP/mzQTv+pZvwaMvbd3+va9fRcRJEbblN1PnUXwfgeMzN4wWV+c0q/idhE7y871q3RS Ta12KR2EoaZqq+dKk3A9YyCLWPjnBeecscwzDvMzjr99wvB0REwtfTXMOZFJ/RxCioiBkJkRLM23 g4EY4wZk9AT/rtSiqMXyWwhCaRJ2jSxhrp76/bUAYMmLmjpEiXlIWveDQtAcHSZ99Oyb3OxEVgMF ZkMftCZJPBwQDgcMn15w/uMzyqyAg5esqb8FSMZoaiZGkNrtcwEvGTOpnHsZE+IwYJgmjIcRcZoQ h4RhGNUhNAari9CiFFoZd1Sm70sneB5MMrX6Wmx+tkmwvPEOePhvnvdh6dSBPejw3/21VxtupMc7 4KBv7zsM3hLpR+r5nilWDUX37Pc0Fb7PeoBMRJtsmyklTRMfYpN6yQBl8P2mIIEskmqZZ+R5xvXL K65fXnF5PWFdFx0TaSBfnYV7gUQqiuhBhffF16cWH0KXW0E2xwqcKN+XYh9pnn6ktPshrdSOqysg bK/f28gu6uYK12IV4easa/3QobynNWjedBs/je5IKYy8rhDNp/9dff+oaamt4W+/19e09yT9j2gC fmQ/vvXcRxqIR9d/b298RLP3Lb/1bWMWEYh5bJsRvQ74nYEX6ZZvexCVxG+J7RYzu7Zk63vRwEXo 7rgj5ECrU+IIxmqtBQAJmoWyrBkLn5DXGcvlqqGrL09I0wiJbJkl1R7pUjRMtT8dDzWPRS0fbVoM BxnAdgI3zM8Ice9r4b4LIqLFncwng4tVdsWOyDKj8IJcVsSoGUb93loMTuFZD9WUuul36p7SfEXi EBHjk2pxxgHIGeU8Yz5dsJ4u4HmtmqDoaI6br75pgyEkKJcr8rKgXGYsiRDSgDAEDEnTtIenA0Iy qTl2obtdfg9VZFkopI2l3y1a9Mie+fqYe9TNnvnW8e9syF4MrZ+rHkzcU2d7ufhHBKeBk/0vbb/s +30PRNx7PvvQpE9xX6TGufwZQowbjub+DcNhQBpHq0Ks6ytSi5yiYg63Vh8nryuu5zNeX19xPZ3B 8wLkbJk4fa8VXQcw92IHt+TA1rRENgZtpt3LoxNXbiSPXoPxPuG6UcO/o1X6Ee1tSdT34UNdwlfe DNUhur+/iGzq8nRks5pKqjZUzEnXLueReyb3gMh8c9as6ccFAKhfTt/cfB3+SND3ve09TcKeRrzX fgUYuevD8+Dzr9gDX9tS/xBNg6D/6vKVRmzrQxFB8xHsriid1tAuRxu22f3g95CwPZ+4JY8hwH0z uCM8Qq0/IgwuYtoLJWOR1Eu9zAvO1wXz6YTj6RnHTy84vLwgjANCipreWqy0dNRdF6RFMuxzWOSc K5NPKXV96IldhNue63eBkEICgJqEa11XLU9tf151NXh5cmHVPJcCLhkcB0Srkqrhm9G0NnqPwgLA 6qB44TRpfipEARQThpdnBGaEp2ccP61YThcspzOupzPKPENW3kiqrRCZFqBKROCiybfKKii0AAFY 6ASJBP4yIgwDUhowOCgaBwzTqIXdSB02Y2w1X7yfOnttvd2szd37Pap3sNG/v2deeGuzeunye23P 1Pp2T6Nx7z6PiFjVwPR393mgllGzz3DqQNR9UYKty0AdUyffD+bjsyzI84Lr+YLr5YzlcsUyX5Fz huSCKFK1HTolDJjjbfTSsOi0EhthALej574SlTYYy/M5c/DqcvYd2v5IMus/fwsz+xqfmo+0XvPy rc01Fy5AsfleCHzIqI2pf9yd7+uj1wJTxT9qYs1rRlnXChY/AuzsKYEH+wPe978Ag/se6fxn3fc9 kPM97a8CKgB9zrRRTnQY4N7w7KWrHjkDnfRZMYQvbI+uMKnapbDdTerAi1/DiHCn3APUkxqkjo71 vp0KMRhVdOkqsECuC+ZckM8XrKcrppcnjM/PoMOAECM4EIpn1CsaMpesHsmaM/K6GtIvWA1gDMwY UqphfmTankfMw0uju4p+HEeNFMkZy6LXF+nUodUEos5dJbPmjVg1T8YwDIgpgZIVCIgWsurEXUwd SkD16Aqk6nOLRhmGAel4wPjyjHh6xXq+IH9+Vc1GXlEKg6SNLYUAllLV42IaFGYBrIJpmTNKiMiB sARNVBaHhDQOCMl8NVLEMI6a6yNEIGouBSGbhx2ouMeo9+DD149riPo56F89s+r++/pXcLc9Agu3 B761yfvojB2DrOfj5vl9UxKR5o2wDLpiYilRQAypAl7ND2O+QIWxrles84y8rLi+viLPK5brFeus /hTwKqdwZ0WuGsgAAYVo77d961ZbfQbaz5Nv50oztsDbAcVHif2PkNDeOveeZuSXNweP7L423IQ1 IpvzRrbFaF6lxd2lpPvTL5TG5HnRNOOd5vlDrdL0fZdbjqS7CLE92p/SvkZ78ZFr/ez2tUDkZ5gJ ++t9bV+SiFSVmdtCq1ofwCMJ7lEn9iGAt9pPgVCXVAdW6qpnFCAzz1qnqPFG6cwo3A9i9+CBSEMg Cyx3hJpqeNEkV6frqkm4rlcML08Yno+AVRVlES/4qc8fAxIl7Wymqr3IJYOFkUtGilqgLMRolVm3 7IN6kGX1PABU7QdHBTG5qK/BPF/MV85CPqtDowKskgtK0Vh4SlF9MlLSJGAGuNiT45AyHi+opXMi mpyHgFUERBH0csThmDAtz8DzM8rlivPlFfPlgrKsgHhSMs17okyE6qIh0yAFtmiD6g9QwEsAhxkZ 6nA6DKMVVkuaJTVGxCFiGEZgSKAxQTyEMrqvBFXmFKrPQOfU1knyQNsI9zZEX5PDz/eoFRFRLdgG R28BSH9u/bz5zTd520X9BRVjuslK0PI4h3qcA9U67wKrHdH2pAOMPhNqBEFy0YJ96wrO6ktxPV2w WBbXPM8W8aE9G9ymWMyhNjhENXBBnUOspfJugZQ7YHFHg1Al8Q+0ewT1Pbvxt2gtPqIJedjeIYl7 f7V6yh2Bf7ti2vlk1ZlFtDCjmDZA/BpEHWq7AyYqk/fP7f5uLlzXVQUVo9OFWcPw3368D7dHY9z2 xc9rf5bWYn+PXwlM947vwH1t6v77e2PxI/qdqgux36hdvn7eLFwbMNkcLPVFEW3oYq+lSeD10ntC 3OPq3eIWVCdPMqZPrt+T1h9CxwT6PtcMdIIEAiggiyCfL1jmK/DlM6ZPLzj+9ozD8wvGcQSHoPZK kQq03JmTmZEsI2c20wAiQ0pCjEUlyIFqn7xDzhSIUP0MxDQZiFpoLYo6gIxBNLNmVnNMHT8TVcSY eckM4QysVqF0GAxoaF4D9wCvkTKiSa2InICR+lCY1IowajTI9ARZVqTrb5ivFyyXC+bLFcvlosTI XDCVN7oUKjVSwKqUKGjx7K7Mmg9DCtYla1KMEC3nSQRFIKUJlALSNAIxIKURcYxIg5lTLFW5xJYd 1fly9QYI3TrcrelqdvCU690K3JTZS7cSgBLrtubcH6Iu/Q5cVPm8GroJ6rOgwExLurN+D4ZmROs+ F90jngKeRHNDiBBIGNH9UcCGvRkoDM4rZhHMlzOW5Yp1mbEuS1V/c9F8J57zpBa6Eg111OnrYrW8 sJVrTWyf1uJiO7BF5Mdvtu9WvrC14tRlc/gDevbIme1HSmrvgQ3vYFXA3jnf9xWgGjgiLR/vII5g e78eJ129GCeYpNrF0FZkYXWIFt//LvyR90o/+JgyACGqfi/kYNZoUiBAcka5zqA1a4l7fWiNOMIW xN4AgTexgQsBtwe0Md46sP+M9q1msh8JOu4938945j2des95df/+o9f+2paUwPSugbbwO7NFI5b3 b1TPc3RN2By/eWCD8Ntr9XcnsNyLwHCtiDMzPfYezlaeTRWIkJlR9Dcl7rAohJJXXNcMzDPovGB4 fkJ4foFED2dEvR8RIaUEMRWxMxrORZNSkTrZiWhK8BjV1r0xb1gfQBpWCpPmhaQSpik+gXMGrSso Z9NUaPlzjTBhVPMLAyIFpQiQGSEFyGBalBg0/FdCA2GeY4NFs2ESgUI05qhjOgdCmCLidMDTpxeM 84J0OiOcXpGvM8r1CpSMnAsCixJQUolW2gQAcGdKU60TIYUArhpeM0OwVZhdMkCE9bMAISBENfmE NCCMCWEYgBi0su2gYComzR6KYGMdg2USpZrl0+fO14RG0rR+7pe1eLRCTyDJweFjxN+DC9c/QByQ WBpnEav/YWBCjJSTl5G3KsX2vTIDPV6ymp+CZXBUwODVdwvWZUVeF+TrBVzWjW/JgAaAyPagQyyy our2kI0pwbcNYfNfGnBr5k4Dmnu53b67GavdSO2/v4lt2I37r3Co299Xv6ebHvt4uuaJCZBAiBZ3 FLiBqUCELJbWO7iJ2E43nAmoP1XDdQYuKgVsDp0EUnDQoTg2IBEENWy/0RtBogCZZ5TLBVSKFWrU tc3waCAXELYAw+ebqF//H5uLD5kUv+E6+/aW1vJ77vs17as0YW+0j5gk7jlz3vMF/Jb7vvfdWy1Z 926Wx7eoHN8jvL154O0mu3edhLI51aMxOgdAaLTIm9cWc4BzYrCumL8U5MsV9HnC8d8XxMMB0zQh urnEtQ0mbcRhQIhJs0yu6pPhqsfMMzhGYIigqD4ZnbXZmBdVrbdLCb1ZKcSIMajzpkeuLEvWeh7Y hmMSokpFlcnM1VSSUkKInovCzCPdnPg0KS60UFYA7JJXHJFixHEYMD0dIaVgfv2CfL1iOV2Q56tW VXWnXGbT2Oo46/MV1ys0hkUAhFFQVAQjC4dzlsoMLgtkEQhdtGS8a8RiQEhqikopIVLSjKJRq85K JCXuwcJ3Y+hK3hvA8Odzz9duA9wwlM2HTlK/v7o2mjuvu+LFzkgszwTU9CfSpFGBQCyTHQOAOxEX Vofe1UDmsqh5Y13UCbNolkUuBcJFgRwz4p4N1vVia6yTJPvXPtl67zhaQcTumWvejI0uyED045F8 TAU+RCP+vNYL7ltGRqiVSQX1PZqcpsBDTwBgwolw9aOpGC+2sHIGVBCANNwOGGhlK5XQhDNQ833z RPHVWZr0nDKrUy9s7ZCIO6v5g1VAVFsFNF8xgp4Y8gAAIABJREFUQP3XP1DT9Fb71eaIH91+hibu z2jp3pePVI4PNRebiXwne1cnKd731O4rqXof7kSroFOufOViEmat2klKfjWJlfoxYCnIc8ZwPEKe nzF9ekY6HhCSZqPMtsFq1U/3GSCysC6GlKzOl3nFaqmzvQJrCHFTilkLCNnTGkcq4Kp18cgU/Ruq z4fbTPs5cOZRuNRkS5qnwyJc0tDMCX5eZTgGMUWlrkbeNbIjjANSCggsmKYBkjPW8xXzScMXl+sV Sy4IBIxWAt5rkSjIMHAl1teOoQkJQKrmB9m8CFDDawVAKfDU9FQYtBZNOtaNWyQ1mSApwCAr+R6S JgKrvgk1YiXUOQnuaENa4M41Q0169zWJDUDDHSAiFVRZaHc3xsLSUqCzlrpn1jo3xUKTC1uIMhfV Wq2aObasBVKyARautV3cf8MdLIUL3NFyA/jhvIvudb09Uv/ex6DN4A3j2IsCPs9tzvcgxmnBfUn2 Qwzsz2xEN2NkXwMgk/ydgknNfKyChQo/rsnreDk8IkiIgBhQoHWRaqC6uFBis+GSaU9PrQ81W6r3 1NZJCEEFhPMZ83WGiGcONs3mG0zpa2jtezb9n93+bOb6K+//Zz/ro3YLLmRPEG7bHhlutBx70tAR mQrhaX+dvfqmBzaE5iK5LWB1a8ODy8b1Kve2QYDnG2J4ZF1lpAD4OmNdM07XBfP1isOnZ4zPR8Rp RIxBa3745oZVSB1HVWVmBues0kERgAuEAyInRAyIURBTC90jQbPJitpLg2kPmho7mM9HgohYLZJi obLF6Iw9BQmimzkKIzOjmNkmJgZiwJAGaMQK1TLojay3aauqVzEVOkH9Q+IIGhPSOGJ8OmB6ueJy OWM+XyHLArlc0UxAUgmnQxbuwAagDrQBpIm2RGxunJH7inBzG7V0xc5UnZCKmkfWed4GbIRgtWJU qguxZfCMsVVkJTKNR0qbwmnVf8bWfO8Q2nBHu2FxxyppDEBEau6C4vUcrLZDMRDoWTc9NbwuUVsD do0AAYQQSDQfiQNC0WuBjInc0Ub6M+zLwO1bbwq5+b7bphUivqHu3r7H9gJ3jvED/5rk8v0WANUk EKD7sYEKrW3UjnV8FcgypJJSujAOoHFAIcHKZXPe7Zw2zZdfsw/99f3ruYAiCGVZcDmfsC4LYkOc zXRYk+5sZ+FbtQHf6v/Qt78qA/1V7ZE25q88Lmmrzvx4u2cLqmCA7j9083K34+tx+2P7JEZiqNuB w51FLq0kvDpIxYphenro74m0loJLmJrMiQyVMwYiNXdcL1jnK+bzCYcX1WIMxwOG6WAMyx3rRKuI jiOQGLKon4eI1XuwjJ+Zi9XkSJreOwR17qsj45nyzPGT6y6vr0SEcRzArCYPL/VeS7o3umIMRylY EYuCCASOGsraqrFSvScAZMsE2NT1en8hQIiwlgIS9SyPxwnH4xFjfsZ8WcDzjPXzZ8iyYLVQN7FQ OjIpiWqEifqtNTpsYKHLINmEW65EUOstdLZvt13bXIwwDQGRrg2WKtEBAjEzlYimXHfiTZZzwzMh BrWbbNi0oAvBlfbq0qxY/6ojsz2Tp8pGZ4qqEbcCCFrK75qkzYA1mWOoAwaWlvbex7E3u3nBsLpH vfPOPHbah7ov6ucHNOHma9lIum/Z1O/u850c8t+lRVu3EtSEyt2fKhfMbGJmMgpqtmACOBKmlycM TweUQLpvQfBsf7pXFKiSmKAEu0+llc2koSXVmxaNimC5zLiezqoF8z7ttDG94Pa9eTv+p/2Y9iPN Jb/CZyl9b3e3Zoo7WoidpgK2uZoQs5dSlKDeaCmgYKAHGU1FuG07AWtzlV5jQDAHJnIvcN2MyuAi IkSzGL5qIq71dMZwPODw/ILh+Yjx+ASKAZkITGK20YAUD8pQ11ULPXlUiWkTFsla9CslrdBqqb2d YTaJnzbP6BIws471YFk/o0WveKrsdVnamHVEg7kArNlLqzkna3rzkAIiaeinD2I/tjVU069JCoqy RzMMA4Y0Ij4/IXx6RpkXXC8XLNcZ6zxjXWaU64KSF6SYQFzU0VbYUpq7L4JAqtqhj/ggY8y2BoQh FCDEYOirA4zk0SzS1gmELQmVaRDg0RlF+09NmxKcEYhUr3s24szK3W/ARS1pXQEzQ2q0DJl/g5oz YI6bhG1NFnFgzkbQRUEQWX8JMF+hukhq1AmRVOnXLrN5XqCBVlN+tP3SgRwH8/f2Tv2+mi8M9N+A ip2vxYYeSKUHgGxv1YjCf93WPYNTsg2sEljEDlzSUUALAWJCej7i+G+/IR0my7C7pYMOSiBbCqn3 EDApCI2iPfB1o7n2BMgZ8/mM9TqD2CN/dI26hsykio5ud+vjK3nSX1my/r+t/eq5SD8y5HgvKW1s qOQi3o7WdCds/TwY7gTnTcPlmr2eut/7HBn7tvXvdonSf3MwQhaxYTZS9bbTstIAKGfwa8F8uiJ/ uWD69IzyaUF6OoCmERiiqjW9MmEgBBowBALnAMnFwmJ1szML1lxQUlbtwdBql7SBeaxSdPDR+2U4 +EgUkLnUku/9syt9CGoyKYxC2XxH1CEypAgk06YYuOn9YBhqSgEBzCpxu5QegoaK0hCBw4Tx+QlD XlGWjOVywvXLCevlov4Tlg2SitMs9W4XqF/L/ln91egxQAKmUhNuSVAyGhiQ4iGaMASgdVvZSbCw mheMPAdy9X5L1175rGkOCBoVw2SvVWOAzauXlvfRqpNZtW+Wp0NCx8wtPNguUoqZN2gbTlslTNPI dDolW8SmGZL+OKmRMW2tbyM69nzjoc+UPU/ojpM75/T7racJ7b3c0Iq37vtXb74XnR97BIin3670 zQCB+2TolwEFhExAOkw4/u9/x/TbM3KIyGWFOPB00+QOWLQ5aKn0dRiDrWsBSAvUEYvWkDlfUOYZ CW1Nikj1/dJ+bRmD+8h8CFsIIPhxc/lfdV38ldqfMYbpvTXwUXvZo+iSRqR7Byap33d3wh4a33qb wykwHJU3dXevOemYhPQbwo+zPu3EJrIY8xA10ZVWCwxIcDMKLHLjAi4Z83VBfDrURFzxMCGkgMJZ TS8xgGgAhQgOGcVKukcKtex5yYylZFCOSNNQK7RSfZ7bhbH/fhO2K4J4OKCwZhJ150+vqAmYhhUK cLgwmApCYUiMCDkgDOYAGZMCD6LqlMZ1jrn6RrivArOaX2aow2wcB6RpwHgEwnHCcHxGmTXKhOcZ +TKDlytyVmbvuTcac5JmQnMwBYCCqNYCYjkD2OCNMulIlv7cQnabB393XX8aUqm/J9KqtkB1nHQf h+Agg6KBE5cMQwUhAS7m7YgzzPxGQHAnOmMUbud2588UCEKhSrfqAOygxc0kXG/jGgm9ZmcOMURC VfTsdwG6se2YXR0b6f6jkwj6vbvdP/XlK6Vbvfx/BQbygQcjVL8pN4lUGCuqsVCtGswmGFCIwGNE +u0Zh//n34FxwFoKsieik+7iMPAmDiykai3qWpDWUxcniAAURp5nrJcLpGitGOmmzex3H1aZf60v xbfO8Y/w2fif9uuaz1Vqi6q1R56+/n2tbbCbdP+ObwqXASav3HynN9F/4gQrbEPb9tEolvJHvag3 G48MUDh57K8PVCt1Zcrod2HtQ017HMmc7rxvoZUMX1cshcHLFeFyxng+4vDygvR0QDyMgKmvycSV kCIiqZWEWcxRT/vNIih5RZaCdc0YQ0SqESKtSFqTkO6NS3uuaEm5htTCWOsfl+pwGBCax6QISs5Y syBm0sykUdOTUzKnR3P+hPk8wHxO3GQSyLzdQzPvFLFM48OEYZhw4Bc8vawo8xXz+YLlfEaeF/Bq /hnVb0S6+hhbjZSwgILAPRMYAiru8W6TSahkt2m1nGH2K1Hq+nAGTdRptEzq92ur7Vo1IZ7alsxB lXxN+X020rz+Frv7+vS1afSoj067QI6fNzL+Zv4VVLT7hQ7Et1iP/lQbj52Zw69Foe29G5PGpu3A ebv0zfNrf2/H5R5w/hHOf/tcB7326xHzfCtUsp2zz3OxA/ek61FMReEkTWeBTDtggdlEyCBkIowv L3j+f/834ssRS1EnXyGlFS5otGXifjxtJYvdW3uojc2p2pekcMF8uSDPs9Imu0YPg3cUpe6H29/a s/dr73tt+W/NT39PP/a/QvuZ4OjeeN377hGveK9963kAvLbIY4b1Vus3bPsOwO32Q9U4fKNU0/pl hK9b9LKlaEAFF9UFtDt3269qNzdOUh2fqklH7e5s6F/t+sZUmIGFUUrGvCzIr1fEw4Cn//XvGA4T 0mFSNYFJMJTi/8/emza2jSNdo6cAkJTspLtnee79/39vZnpJYlsiCaDeD1WFhaJsx3HSTroxk5Yl kiAIgKiDUxuCdyWdO2KSmBkkabE5M1JaMIMQLXeIAow2dfi1/m+Hzs7zyp6E4JHSAGbGw/kMQGw3 WIGBCSiybSwzUpwR10VYiBAwjBN8kEycWXf83f5cBWNJPKY/isGkAjsH+OOEMAWE21vcpIi4LJjv H/Bw/4D1fMJ6OoNYfO/N88Gb3oDFe6YcYxl/26GVUSvgopkaW6F8MR8UxDRjj2ba2rtSAInRxN1n r3K7qL8IvSqr2zE1XPD4a7IvKEvdjH4yNNdcfuX+s+ZD31lIriwsV8/b9P83Egbbd2Svjz63nucW E/K5EYAyHpJI0c5ickgMrGC4wwE3//gF08/vsDqHJUZlGakHusyqi2ytzqyttt5RN4edF9fTlBJc zhKXJkZd4yrW7NiRMgEbr5dyN3kHL+fX1yvfC4i4Vr52+58DyF7rPp9Tiivqcxr3HACyq8ooDMIG YLRuI93ZzazXw/2D5c3lGyRr7Szirbq8lnbKg2yaqXrNXHcDYouBTlhlk6JgeAY4ZlDMwBwRzw4f 5zOm2xvwu3cYjgf4IUhwJ80X4IgQHIGCxNbISYz2BNA4pJzK8/iGwTC7CudqxM3qfdEIzVyfl0iY DMDD+1DqtDgZ67o2AXY0yJTlDoG0jSODY0ROGS54hMHiZeg1Bk5ZhLrrxrMZQwCsKiEiD+cBkIc7 jBiPE+ind+BlwfnjR+R5wTIviMusaekZnglkrFmxX5H7Og1/nB0QXY0t0DShKbz/MxrA0Uy3XaPh nd3IliEB7b34jX6eNr/1zbto717p629ixGzaZurB7knsvdq8X1dtfHbasW87sbcG7LV3//hrl8+t 86ld3/Vj1GRTMHYBGi7flXeSySECiI7gbg44/OsfOP7yMzCMhVm096nbFukmoA1VX9SIVI2MzesK TcZkQILsredZMt8C9Tqrq4QQ35/vF7HdYX377YDG36WWbwG6vgS4BKPXnirXbvDUoqRHrJbmXldY DGpc9S4P7tZcgi3BQHUPNohc387yl7qwghrBUIMkpYYdMVcxACW6I0FsMsCi1yZ1uTwvM/LpjHh3 wnhzEMPGmwPCdNAATWL4KP7tCQlZ06JL27wLYKg9ROYS08IYjGEYAKC4kJanEQtLUd3sjoWcP44j cs4lcZp5miS9HxrDLhFIorqJywJeCWlNktXUC2Bx5FW7ImOaN/cWO40KAi07rAk85wnkJwzTBMcZ x9sbpHnGfJqxnE+I84K0zsjnFRyjhFN2DJ9FReZ1FC2TJ5FDdZ9rFSq6uO/0TDmDqARWvHJGZUDQ 7+mu1bf921xVKytSIyoaA/M5hTXibG0hgGYOtKB779nr8W3j+eo73aotuzrQvnc9g7Ct63vfkW5L y6QyTNDXxG8wVQgRFhDoMOLdv/6J9//3b7jjAXNKmrwQJTCc2VXUmsvdKjglubfEG+5tebK6jAfv cfowY50XiVvDPbBonuIFWOHrjeNbmSPXNhJ/dl2vcf+vdU2widiWvYfdIpjPu9lmCRYYLi/gFURc z2gv3zAUfa3WsCLE5FgjaM2+YCMcuDnHFgNrW2mDUd7UxxcAZ1C2GP6SaXJyhLSuWGLEcpKcHIfb Gxzf/4Tx5gg/jiDSZGXBgT0j+4R1SVhTFDaANdgUA4klR0dKCWLTojEmvOs8TGq/9ONXf1fmxdJr a3hwZu7iZWTN/pqSZjRwFmGBgZwRFwZFh6wRL4OCjGB5PpTZMVdIRxaxQXu7pJ1V4UfVKI1BmI4H hGnEcHsrOVvWBfPphPn+AXE+Ybk/A2lFjBk+q0cPSSCirBbyrAnrtpv4Z+l0H1ldLwyEW0H6xCvR z9P2k8s8fdkyY7MYHaiw9rafaBivy2r6eYI2iVl3Wn/eYwDkOfrg763YunHVdgPQDYcErbK5zwCy I8zE4MOEm3/+jJt//YLpp3dYiZDmRVzWi3Gv/rM3g7f3VLWkAlTzpgPaEWR5L5lxPp+RUyyshewr 5dOSDl6XBZfAsG7i+t9LH7ywvBVAYeUak/dUee48v/aevKTuPVOFLy0vrSuU7dMjFe55JzzWCFvk nlM+Z7HZa9P2d5HxDfVBgOUSaTdo7QJrLnZGTVfzPBMarKnG5VrX7QBNkJaWwWs4S2LJWxKjRPtc Twum41GYjMMB4/EIF4LoZz0QDg5IHuuS5J6q/jCB0f4TJqMPDy6AxdXntxYVUFXBXdtnxoaY2iUu K1KMWBRsyOKDokcGICBD1TcJBO8D0hDUy8RL5lJ4wDMIXvvY+tzaUQUeCqVLmFOUWADBi+fKYRRv k/e34HXB8nBGPJ8wP5wQz2dEzbli3hqOa10ZNf6EuIqi2Nk4rvEp7LOfb923tgcvyq4QxnYR7ufv 9n26JvS3ddt5rI001sOCe+0t9i1wucYgbJa33XPaY8+pp87FBjjx48vDWxItu2sT9fCzO4dN0Nfl B6zMpyek4EHjgOM/fsHtv/+J8O4GyTmkJKwbq4oUmQsja9C7GGDspGyxQ9Kg2iZjJ9eHB8zncwkN b9ehucYyq355/7+lEfyy8tpA53OA+PdewlPAwr5fYzOA7cvFRSDzCzvrGohobTQeQ3vFrK/YJABc RKR5MnDBVSV+hgq5zA6QbNcaFbFB7/qemx0IKz1pqZPbHaID4B3BszAZ8dMdku7Ax5sjhpsbscmY JvghwAUnsSZIFpsUo6gpiC6ElLiXCuNgrIH3QRkN3+2sqr2JeYlofJCun1FtOkZCHiS8d06pxMzI KXZ0eF08GSkuQI5gR5o8TFxZPRw8ZZATLxLBerlcb8Nq1vBEQASJe57WDWLQ4OHHAMcHTD+9R1oX LKcz1tMZ8TxLbpP5jDQvCEvUIEI2xrab0wFnLmNaqGuu7WGNjPqc2VvO2i7UqOqha/OZNpdw84nN sd3vzJDIn3xxsIMCBURr/bsLXL27vAf5siFdfdfBB/U337SdisC9Wv8XrrH9xqHOgbLpeUpgkLX0 Bfe2y+sMU8YCyCGApgm3//wFt//3Lwzv34ODx5IS4pqQV42Fo5FaNafdZk4ISpYhuKbik1g0CQmH YURwDnf3D4jrqs+ujEeD863UvudHASDw9hiGb1Ge2mBvz31sI/6jgYltCfoelV2QlH0g0bu+XVlc GsO0yw6s57S/PTUI251CfdsabWRpDlfth92zrJtVaDBsEbTvwk7kco1elGtzi/Dptgm1AYViJFd2 HQTxdCAIo5DWiCXeSYS8uzuMxwOO79/j+O4WwzgB3mMYxYV0IUaMGSkbQNLFxqnaJDOAhJyAFCOc S3AOGA83AOUmEyrAuqUtgsNJqmYTFJLASNkdR3AuwA9yPKSEJa6g1Ylr67qWQXBcGR+oKiKmjEwE FyI4BrCyKxScGoAqmGiEvjEazJI7xYCFJCarlgik6hY3HXAYJxx++gmICfN5xunhHunhjPzhDlhW xCUi5VXHwcZFd3kWcErno4EkOYHRTu/9wGz1P9J+nTcdRLgUrHVFb3fyXGcl17l1aVzKhg7kz2xI t2cMig1H+d6yHPV5iuAFZOaz2KpwE610v1wHFu3R69Ytj9u9fGnh0k+1Oy0TcZt3Y7um6HKgL3zj 9VPOtYHRNWm7uVGBTDo2TAT2EiBrJQd3OGD65Wfc/vv/cPjpPXgIiFmCpsU1IsVVjLqbYQZpzAxr YKNCJDRROrWxkqNHgvkFH3AcJ+TzGfPdHXhZ4AoYNXalieDbbRqqW3SpvPnogHGD1/aMfj+nPMdt 888WzC+993PB2HPO+x7sl0Kh3IC6Yj66Y3n6971dUzlCKAKmXcH7CXP50raAxJgIu3qbEKiCARNa 7VOS2jTUXZzpy4mMQrdFl/X/1FXMoI3RYrPAq6DyVPNXZI56b015zgyOK3KOOK8L0umEdH+D4414 lzhVlxzHAdE7CYalDELmDM+ugBhYquasWVCRkXkWNYvmDvGhJurKGqOBQGWoScNNk0hgZFCVcSSh vkcnKpicM9KiuUzUg6QsUARwznDOC9BYMhKtyD4ha3ZY5yQKKDk1dLPMcah2LjlXNZXYstS9oAkA trY5B/IDhmEEHY7Auwh6PyOeZ8wPJ8zzCWlZkWMUe5aU4Yk13oBRz3p3BRwAwCRAqIAOVLmUc1aj Xu1DNmNfc+fNrURqpgk19TTzuXzK8zkuAZnL9KozvarIjINrTYnaPCfb+Wntr2r0Wk8Fw9bA/t3c K9d00d2vO+swP3LsVYru+puOLXfumtytKfZu6/tO7Vg1lzSf9n7ILQ3o6fjDIRMQwVh9gLu5xeFf /8C7f/0T47t3SD4gZyAl2XBkCYADC/zW4sm2fQ1ygy6mKPJf0UAmApzDzc0NphDw4f5XrPd34HWG VyZX1C76fjXrnUSgrXOvB9mtrLiY3HUOXVFZP7fsqRG35UuE6V79z63vc4HFtXof0wS8RrnWh1/z vtt6dl1RX+sZdycZP2ex2u+Aqx2GfnNnC35fQfOHWksZaODNKZekRDWCq1ClL1tgdEGFs73/jS0F a+CqFJHXBee7B9A4YHr3DuPtOxyPRwyafTSmhDmK/QaRUO6skTzLDlrkI+KySHvdiuw94uBLojIi QjIbFF2Y2IlgsyBYyFJ/m5SKnEOw/CcanCvHqB4m4lJr9hnUCjywJG7LCbxKO4dxkCBdIQjQ0J1i WT+7BY3KYlvG6YI3J4Ac3DjCDSPC4YgxZxzjirhG5GXB+f4Bp/t7xPMZHFfRPavvf85mPS9ZRGyh NbdXYyXq9FQTO6YSj6DESjEV1sUMqfOmDRFeZ0z7LH12ndaeB80MLCC6dI4JOC5XcwN8u3bUrWbt 8+I98LIF4K3snogtKmn/uy4NMkZb9ZSiaYJuMJo+KOuLzkMzoJacgCqkNWIWg5EYYO+QPGF1DsP7 d/j5//v/cfOvf4DHAewDMhNSFhYwRUnu1yWBQzMKm4XJQC0sgie5AhAyZ5AbME4TDtMELAuW0wPi PMs9tL5unG3NVNaFlbnbm8NfgzF4jgD8mvf7UctLGJDXLAVcPCbYX1Ku+Yq3KpWrNhObJlQmc/86 211aDtFrFrN1w5a7Xy4n9+XzdGChCIm+LVcN96C7BXthcy47DgKJuiRHpJjA84J5XuE/PeB0PIqX ye2NGFyGEZE81pSQKBs73o2dcxKshzIkeVrM4HUFeYfkA8g7+EGjbXpfPGgYEk0TOSNQuFhg235w wYO8A9RGI+dcIoCW7KyoY13dLKXM5wjyDqv38MZmeAdvUUBdUAZI2papHdOmX6m2qfzmCKuTvDQ0 EnweMPAR4/tbvFt/AZYVp7t75GVGPJ/E739ZJbZIZjhWgAFLVlYXWnsOH7xkKml/N/BBVIXRthil XaSc9Tw1n/Uhs9XV7bBQWJFOrUcN05HLf/r3Qf9bAWnzTmzG+HstBOseuhwEMqHt+lVuwwo4rl4Y AEoamGwgHhqsqjCwAmicMgYLAasn+OMBP/38C97985+Y3r8HxhFRo9eWd2ZZkWMCci7rQdOUWorq zFxOa7uryad8Bu9xPBww+AHn+RPuP91hXRZNnqe1MxqAxVcmbNtFV1t2cd7T5bqL89/l65bHNulf Wu+27ICLOsH/nPJ0tNCrVNMTx7uTmvPazpad6tYcmzbt4U1V/WC1AtA+bTfbsQHk4KmvjQGkecWy RKTTGfP9PeabI463t5iONxinEd45JM6IibGkKKwAs7i2Og+fiq+LpnzXmBk+A94hJQKFgGFg+BCK /YNlULzof2VZrOUGHsx/P3gnqeSHgJwZy7KAORdWI+fevgEAkBmJo9qKCNAJmvhsGOXeTpmWusFq hSz3mT2JNFYIg4IF9QLIaRh2NyAMAe54xOGn98jLgvV8xnw6YbHsrcssQcWWGQB3QoqYVV4zOGW0 HkYlHqkB5x25VgTV5nvlKOSTWvWFAQkdAy6Ta8MvbGQD0faH0mmVndjQ15+74L9Zna+O2f7KUdmJ wlpeWeqoARhsrAbrd2XKLJst2IFZ2L6YGelmgruR7KY//evfOLx7h+Q8IjMyQ9/dpLmGEjgntV1S QmK/5ShqK22f9XhmCcFv78w0jTiMI4gzTsrYpRg1dUHucOxF5+3d+wnA8Plj38ToeNH1r1v+7Pt/ jfK1VEqfU0/Y/vA1YcVj+qceUfXnP5cmq+yxVXB5XRee+Zr+iVN3nDW6Xm1jAweUgq5LfteS0hzW P8gAjHCYYOJyXIzBuOROQcrID2d8Op9xvr/H4fY9Drc3GI8HwDsERwA8VtIYGJpLQFQaahPgqCTE YmZwFlBCKSPFhBA05bqTiJkWY+Oi35q+sjgZEmmTpOEa84KYMXlR+xiL4RpGw9RCVo+0Sz6jPndK Gc6LKsdZvfYs5KpaSNtZjtsYZEtcJzvUzAAoF7uI4D0oHBCOE/xP7zDFiHVeMM9npGXBen5AWiRV vO0sXRbPH6e0t8tegxGJwyvptMmcQM7VpKdlYtVpUalpG3/LLJuLcBS3WNthqhrEBEpPvBUQQhaW 3vVArEJiA79iY9Oq+2ojny5vejE2YKdxJgBUdo7qe9aBvDI27eqgv6nAl3eSLIE5CB4gEhUIKhjm IWD8x8+Y/vkPHH96D5omLI4A78AgpJR/OWpYAAAgAElEQVSxrgkppqoOMWazWTdMRdE0Q/5mlPfT 1COZWcLqh4BxHHFzPGD0DsvdPR7uPmGdZ209NeB0b22UG9Du0a9X/kzjzDc9l99weU6/KbhoLJ9f qa87qnqjS7x27lN11d3948Kv29L1Z4Gbn7nZI5jef7c9Fwvx9mVo9xpU6cpGzVOM1alpQ8mfoTp7 XekcQdgGUp1/Zqz5jDhH3N9/wuF4xHA84HBzxDAdMIyThPO2SJtEup6qxbouoKxtIgCkYcfXNSE5 UUn4wcE7DzeMu+PUMjEd2OB+12vJ1sy1tU2exsxYYyw750K3MgRkAFjnRcCK1/Z410UodSHIEs+q MsmsKVxUEiQuwc6K4SU82AOgjCXpmDgHCh40jfDHIw7pHXJOuI2LgAt1cY3nGem8IC0L4rLCZ5a5 kzIcZxE1KrUsW6qAhL5cqP1YDEGJs46/gYhGMG6uN4bJQEiddQ3dUeIV9ExPU1P57AwwaXN499r9 dr2VIiBen51sTnDHJnEDRM1WpnJBXGwOrEMkxo2Exid2AlYdNNomEJkRpgHv3v+E4y8/w/38Hv7d EX6aBFCoF5WwemIHxFmAvjOmpRl/U69tC9VhKwjVjK+d9ximEcfDEYdpAsWE090dHu7ukDVuzMZO t61Z2C5Nf3AlpqGcuVnb3tLYP7d8j21+S+W5/Rd4s0i91s0ftci9Zpj5FIIt1+/fs9RhC+bmPEPu hXugLXCpbpLFIp8IaGw0dK+8aXfbrGooetlsNbLLueyIrFZrd0EeWdwBxZ2UEHNGSgtyXpHmBe50 j/nhgMPtO4loGQJGckjBIWbWlO6y66WmkaR5R4x1kcA9AkiwOmQXgQPAjnrWoOnj7dTa/m5rXwEP JLE3hkESp/llKRFAjb1o63IgjbAptiVplcUzebHJ8KpKEWZDxWiCLJDOdpa1302AynM4kHdgYqQm aySrSgbs4McB/njE+C7jNonNyno6Y75/EBuNeUGaBWykdUHK4jLrWPOq5D5GBDWfrWdG+S1D3D+5 EXwF63Oti1AYhwsfRHCdiAps1OwU20Jl3nN3rz2Avvf+vnX//EzcPFN/jFDBxJavqXlB1HMJVBgL YgLYiYcWERIkFguGEdPNAcef3uPdP37B4f17pHFA8nqcRfjnKEB+WVZwUvuKBljU0arg1Fbmcu5m KLOuFaSeXOM4YjpMCM7hPN/j4eNHnO/vNJcIFUZvu97X9UtXxrJAbsa6YW2/Rvle5tePUFoZfe34 l5YAXPoVf8ngtoj2mkvSXsM7hqOsDDv1W92tdN7cv9J/3Q3KgtpS/1QvLEwGFYMvEwImFkxfXZNv CLNQKcqLRnX9oJblGzVQvVVdXMhRIz8yKDO8IwR40deeEh5mySY6HiYcDkdMhyNoGkAhVI2QClar i8h+Yzg4Ec4KpHJOABPS+SyJybyX0MGNesL6uDx7289GP+f98TVGw1xajc3IqjLJWd1aydc2lbYx EEXdEAFtW9AEamagKiHPEqrBbGFsmrFwZndATkOTt/t867gMckJXuzBimG5w8/5nIGXEeUacZyyz BPFalwVxXbEsC7Cu8CmBctJUuGY+qIwCS78DucQ0KABf5yd7KvO89d8QI9ascVYENGZUsOz0XFGL QWM77IH4zeAUhIWy29+Cku9mt6dyOZfloRpC6uHuc6+YobYjQmKNjcKkLKCyXSEgHCaM79/j+I+f ML5/D3cYsXjJdipqSclIGmNWNUgCUi6bFQMOWzsd21/YErdtq6lVba0IXoDFMI4IPiCvEedPd7j/ 8AHxPOs8y2DOcHbj9iYwd+T9+9k9vzSGxbZ8N3PqOy9PAYmvVcLefPlWBh/Xzq3pzvdLEfwben5L 1XUIXRdbsanQ45sb2GJMuwadVYnCbELQrPntNCrC+7K99juX3VOxvy6gquKqLrkAoQrEHCXEtcbq SPOM+TxjDneYpgPcNCH8/B40BHgfZIHT/CfGqTjnS+ZFs5kwQUYg5JRAWSKEptUBmsPEQo2Tcxdj awaU0g1U+rlVpdh5BjS89xjHERxT8TRhlh0eQMhK/5d1EGyx2ZHSirSskg7eewQvbq1wBB4EXHkd o44lAIBssVptHGxUDADkYtshBpAEcgx4DwIwTgcEThjVxTauC5ZlwXw6I53PcOcZWe01UlyRMlAi aWbWfBN1dlqiPIuzITCXdVzqRC0Cn3KPY3VeZbZnckWA7L2De5R22UEbyP2CDcc1L7Dt8ZeuM48v lkLxt4JQxr0+HxTEFukNaDRffXZHYOeRjWdilsByPoCGAeHmVgysf/4J00/vQIcJKTisTiPIsGRC pSzqzbiITQ9yFiCMfo0rWZ+paXH7tWE35BXQd5UIfhhwOB5xuDnADwOIGfE84+HDR5w+fkJeVzFo NiCLLXjo191qDEwX7+5TY3J9xd4vn2uw/9LyVwcxe/28JzdfG4BcGHR+T6XriufMnyfOeYQk6o4X m4OirDV3S2U4uH8p62J45Q7GRurq1xI3tgSJEDQPBonBUGKKskOOETE+IJ9OSPMZ4XDA8XDENE0I wQNwqnVhMKfCtWYL2qOPIhk2a1+llCQEOEUkH2t8Cu8RvNhDFLiXVfz59kkboKh15pw6lYkE1SIE BBADnlYwszA0jbdJBWHNQGq45JgSsBASMfxhBHkPUkNVUhsUghm35sbmV0WPAkUxVLU08oAZyxjw hBPgITvYABBjwAGBgSlGIEa484I0n7GcTpjPM9ZVVChZozCmlEGcQAo6+pwmXDwVBOC0OVnk2S0+ lyTGqt3M1skOlbkhFKFZQFbDutk8k6oLSr5kpb5CeSnIeGoRdKgg3wwgiURlVW3LaAPmoR3jQHCQ DBzClJHz8OOI8eYGw/EGt7/8gnBzA5pGYByQnNheZO3rQA5IjHVZldWK4uYMSFTVznii/ZSG2Dw0 1pEKCtY2cwZpPqDxcMDhcMA0TsgA1mXGcn+H+f4eaZmBZE+idhUs8VkqjjFG4scVwF/D9fJHKY85 Wbzkura8aXBxySxsTuDNwrg5fql6UPsDV9H53v0u42lsmY4aW4D7Rsgxdhf1yGC4vl5jStpz9Xdu f2tVOAT4IiBloSI9P+ckjOvDGWlZcX6YkcYBYRwQwig7m+AlfLbGuMjgEtEvsxqZUVUMSXfJ74kZ yISoNg+W7Mx7X1gkR1TAQ9u9PaPTDqzu0l0V/tNhFHZCgUWKEp3UmA3mxtPEekaFhQOQH84g7xBd KOocMQqVZyZNqVPcXLVt4mmaAXKd7UNrQ5Ohz9ioIzxZfA4PGgJoCBhvDxjyz7jhjLRGrGdRoSzz GXlNSHFBWlcxEo1i4EcsoHHQTyHQJKqBiQMC13gFFv6bVCSS5MRZci6hnK17iGjHx7EHzTY+9uyv BTAujLpfiabdCo0CllhVRI3MlKR2ajtBThUBatBKphRwYGfhswP8OCJME4bjAePtDcZ37zAcb+AO I9h7JEcCKABkNrZB3b7XFetZXJvBJtyNI6tzq3sXmk2JvOsFBdZHIQDkJEjW4YBhGATsk2ww1tOM uw8fcfp0h7SsGggOFWTpfKnO6i2bwd23a+WaWpvaHcQLy9cEAH+DjMfLa6tP3jS4eE7Znyh1B/YY LXztPbi2aLX1MzM415e/rbsu1pv7kcVGqBH+rP7Hprvd35ksIY3pgKzSTxYQr/LcZUZeI3hJmE9n LN4hjCOGcYIbBwyHA2hgsHdCsTqJkWGRQ6k8h1ToyFX9fRa2g11WIFIFtzcmw6ltg0X0VNbB+sHZ LgzG+TTjRYDzojQYyMuxxFhT7MBGSQlvO/JW9cEErBngBREAOZLgYc4D3sH5QWluX+w1qhyy9jSM Dqsuptn0GiVATKKOMDUWCBgGHR8RNuEADLe34JTAFtk0RnV3XeTfukpk1Rjh5gUUEzimGsPEBAND w9MrsGRzZRa7lJy4usH22Lh8qUCFiuCx8d7aWjwHYLzknNdc4C/rzipIi7ULZG5JbIqoDBurcSZ7 MeYlJwn3xsMB4XDEdHOL8faI4XiEOwzAOAIhIIKF2TA2DOI6LB4gjFWNfSU4lnh/OVfXlF6YX3mm orLhOpec2EiNw4hxHBGGAcM4wjmHtK4lZsvp4x2WhweJQAthSzjnYpOzuZGuKZ/f7xdjqP3xkrH9 W+B/XnkMCDynL79k4/Bsb5EX1f5GyraDK3KuwuolZSvqCzYooKO3umbym/OvoRY7nrvrLs/nbvEp NHhpA2tAHDVBYG7SzBM8jGqXYFvIErQnnhcRqrYbOx7hpgHOidogA0hECORk91XiRRAsrgUza34U iJcJMnIiEEUk58QGYhCQEULojUA74WwrWjN+2r9Z002bKskFSenuWfprXUVtUrxNGrABZjgKcJBg RQ7iScJZVDtMAPu1ggsvuU6c2pLIBl/jhNTwWBXIAWBOdTxsFjCK0V9kywxSE7tRULaIGS6zuGkB AGdwsmiNC3iNoNMZWOT7ui6Ii7AceRWWw+l1EmfD7GhZ8stwBosOqIJjmLuz2LIYkOiDhG1m4COv zksXptfcOW4X1t4+wMLi1xxGGU7UXUTg4CTrrjJ2bhjFtft4i+E4Ybq5hZ8mhHGCmwYgeCQ1JI6Q yKmiTtDRzyx2Q6tE2U3rCta5WCG05fMx5qBp+/bhGKjhvJXZ8x5hCBhHyaMzhoAhDPJ+KaA5PZww f7rH6e4OcZ7hlDERe58mYittb1YZxRpz53ICPM1pvKz8DSxer3xpX5pMfSVvkbbsbnXedHmcuagu pK9B9+waMQLKHJitxXVwc3H9Rv1iLy+xCLhWF99S1zmL+1mlT4HGmk/VKA7gDK8nZFVrZGasUXIa xHnFeDwIqzEJszGFAYkZ5Hs2RmqhokIAxKPFjotHh/T3Ghe44IvaxAw4TUURU7qcwMZsEMF5J6HL mRsg10QEHYLotodQXWlTQkzieSKRvFnsIpq5nHUXiNRmknXFM8biaThnO1mxqSBHJREWEUn/m6qB +70+y400voYTQ1B2yJzFQ5VzzeECOccHj2HKGI5HYSKSAqY1Iqrh6rqckc4L4jJL8KW4Iq2rAi0B kEJ9JyAniZmgjE4JsmXzC/3YUjfPLvfUe4vN9rfXtNH40neVS/+6CsBJmQvn4UZREQ7TAWEaMRyP GG+OGI43GA4TnPNgcsiOFISokCcNotW84jkLOMzrWsBFcV23U5mVwSBFgs37hHJq/7cKeVYG0I+S L2ScJhAgKpsQEJX54piQlhUf/vgDD/d3QIoIymwV243SQShzeQssWmbrsmP3XZvfevnRwEtrkP3S d+WxPnmt/mrABSldf2kp/lbKI4E3+/PKQleFfDsQtZ79Ci90uIUwr7ujrXeLLAhiD8BOFxfu42Fc 8hPcCSjDJrIs9ovBdvExelWuy42vqch+Ru4qsN2tA8TWYo1YoyTyckGSox1ujpiOB7DaZkzDACJJ sJSMxYArydNgTFFZvEQIp5SU5QAcRVGReIcQBniNVAhqErhtnk2AAQQ4XIwFlEmRb+QAeA/HASEP SDkjRQFSVGw0NDxyzmWsrA+REjhGZGNYyIEVZLAPcI7AniTNvdLnTrd/ZfysLic1e+fBms6eNYAS KdiAc0od5yL8c8miKjtihACeGOAE4iMGBgZJoSmGu+uCvK4i0JTVSFH+salVkrg9phTFjbcRMvJ3 Vuht74DN8x5c2BRq5/y++GnF1+MvqE75Fy2MBb+jB+XWx5k0voSqOZwXDycXBoRxhBtHhOkAPx0Q bo4IxwPcOCKrh0h0skkQUACY/ZPMbgcP0nmZRUWYxMWUU5JomwV75I6tk3bWuVD1NXTBGkFZPSLC ELzYTE2iBimqPC8gfVmE2cprxPn+Hnd/fECaZwSN+lrVhfLOZM1r1A7ZhgPqfvjaLox/q1D2y4/w jA24kFfimz/U3ib/mfN5zyai/lkX/avvxxOPajvp3QvKDr6eK0L9yvnlMltZoS6vJuqav8p6r8/H 6LM4umrgVWwZynFGm+Gxbb9jdUbNel8TbjFhmWes93e4GwIO799jOEwSCVS9TbICpYSql88QFUam 2j4BRkF2zpGRJLwU4CLikkEeGMIEOC5sBojgnLFLwrIUUUVUGIsWLl6AEie2IeQdxjFITpWYStjx nLO42JqQbRgHu5eAATmfiEAuSmAtbZvXhG3kxLvFdOA2smwAQfvXaW+ws7ki21EDXvK4vpslWQ+w AjVTqzhiEAe4PGLCTXkOyXkiICKuK3JcwWex2cgxIUbJuilshwjA9TyDcwSn6h5rgAOqkrKw9DVN CZc5X2caNe+A2Diw/tXA+trJ9pu5+ZYf0GoCdjYPDdOic42gAILM68gAoXh2YBwwjAPCMGGYRvhB 2Dk3yD/oWGZyasCp0S7Z7kcFsLfPwFAX7ZTFfiZHpCzzpjxnkz6g312WzuwBBdl/7LgcHELAOE0I 0wgKQTyg1JA6pYTldALHCM/AcneP06+/gx7OCMmMfhv2qtRKhR2FjTHUk6YbqG0oQP11ZzOwXx5b XHm3rueWl7JkX8vl8q2UtwVIuFeLXCbs2i/dLmZDjbaf1+/bC2WgpwibfN2PXr+rFyyLAy7Oue52 s6mj+355zWM2FcQqIHbuT9t7Gc1oOwjT6W92Dlur8myhiItA902bqF6Tjca0/jL5kODJbCFqlkWO jJwW3C0z/DhiPt7geHMjEUAPI0IYJLkYWD1NCEuMmrtD25YyBnJATgVsERE4MRIyOGZwFMYh+1Rs HrgTFFR20ga4CqPBLIE0bf+sq2aG2KHY4uzJwQeCdw4cQgUY6mVifxe7EjRADc5ylCGnBI7Sfz6K QLJYH16FGQHCbBCBXNYxQFH5y5j2AbGq6+/OIl5+s+ehovUyO5WW0SYPYGCEgwoRI62yJI7jmMVm I0awsh05KdhQVRLHLMAkJeS49qDDoqharA6DEFkZoUKXKwB0l66z7Zw2z4kWSNR03/abPiMRiMzL R0Cdd+pi7J3Y4oQBPgSEYQANA2iagHGAHwbd7bsSBj8Zs6H3YAPcLNCoeJFYrhbt0Ky2MZlZ0qRn CUol7BVEjcja+bk+LBU1ZX2/ywtjc6QsCvKb9x5D8JimCeN0gCsxN+R5k4YRX9cInxLyvGD+8BHr 7x8wLBJaX+J2VBduJlKg6tDGAXEGNuz+5b8Giuo6X9cYLuf1y1k3w7ezujvP+mb723Op/sfOub7O P17v1wYfzzF63vv7sevfAqC4HDNh9l5k0PnUA72GUcl3WyyJCADbOVAjZfoJbHrO3o2rwIQdvXZb 9gCebYmLYdZmYYctmsqa2PJhHgdySkaaz3iYF5zuPmIcJwy3RxwnccMbpoPGppAInjEDCax5TZJ4 UDhI4jVmaGghYRfgEVMEZXVZjRFurbYOjkhyh5CcT66q6syOgLIyCTCL/SbLeFkEaz+b50rbXwYw WtBhfzvnNHwGl3ylpe8zI+YVACG6CqZL+9V+Axo+3YQkq0AzNZheKO1pNrYANnEvKgBpYQg3bapL vR7TMIzOM4ABbiIE213njKDsCpuqJSdktfNgFnscVuEpNgVyDrIwGzlLjhirgwv4kDlkfXfNNdEY KkBYLwFlCi6dAwKV36RPLXib/HP2zzsJbKV/ey+7+wxCJqkvw4JO6RioMM8NgwV9EwioSFvRZWZ5 XrPnSWI4U+eYdjsV5s1JFFWuwBhUt07t+++8MT3SPgtUN00DBg2C55wHwcM7h0xAXCPWKNFgXUrg NeLu9z/w8bffkGaJa1GSiGj/U3P/y+Bim4BjVLYau+N2bW3+s9bs71VW7Bk3PwZu3tJzbmXS5bPI nHs1b5HnMATbc7cd+1xr8tdGll9iGLO9vm/5U+jddneXthmPG8ypsHmizeWZTKiWFcbu2x8n8xph oVXNCHGOEcs64+zvQYPH8fY9xpsjJlWbDN6LsaRzSN4jgkt0TWcMgQIN0m01g5GS6slTgqSfV+Yi pRI/w3sN/gNZDD1V1z8AAjRgcQSsZ/b7ZUtTOyceLS3QYGYsGvSItGvIibAru8BCOfdAUPqwMjC+ 5D/R0OSkdhsm43QM9xYYe9627dv349roGwMpLrINgHGyVZbwGQ5gAg0enjQtvd66CGMIwDBXVVOd GJhgFjULun5BiaK6u9OCtkP7LutwkanIPMENxsw5lBDtzvJ6OKSkidtIQ3JDkodFpYO8xMcsLAur fYw4i1AxbHaN0C1tYwYn7gBnUjdozpa91hi2ZgxyIxhsfl2Mi71nMgeS1afzcNK4Fd4L8AAJUDIs lnOWoFzLCp8zPAPz/QP++PVXPNzdwYMBUiPoGrSnYf/KbNX/9uPz1M75WvkcwfdaQvItCdvXLl8q j95K+WJwsdcJX9oxf0bH2oAyXgZuumsuGIY+++q2+r0oF7Lz6enber25Otb9SCUw0fxVd9b1SOtq tLWxMffEjAAqcQAyM/KyYkVCPjGW04LhMOF4PGI83CBMgwTzGScMg8fiCZk0cqgJbVd3fSasisEm awAwa2ZORfA7dQ91qoYQ745GP8xKZNsmkvZjhlzoihthDaBkXQUA50MDNsxGQnKv5FzZJSrjUXfB 5uVCREjqieKbXTi0/Y5cib9RbRCojHmzzMM4JbagWfb0V+apuf+Wc6gy8WLLocKWAYnnofwMy9w0 lsX0Ou2ul2orwSQ5b/RG0m620NftPbiMrdjn6N6YFVw4ErWHqpbqztlGWT6TAoqYVxTerZAH6nBL EHUQy3Hz8rG3zEBQyyoANSgbMos7aVKblZTAOYEh3kqugEe71ICVzgpX8xbZPCt/S5rewlaICiRg 0IRjwzCKqk1VLfp4yJyRFvOEivDMCESIpxM+/fobHj5+BMdV536CuE1zUcfU0q8Sl9+tzRc/fXF5 Lhh47tr/3I3oWyrbDeNz1Tdv7RkvbYkufyd65TgXn6OzuqY/+jMRW2nTE034/HZuJ9SWqehtXUx0 FuKhpbzLDrEQuQBcAUbtBK5NrAwFNV8Lfdp+BWt0w7rA68YdRGLXkNYFa4xIpzOc/wQ/BIyHCcfj Oww3I3CcwEGCVnkiCT+uaglhLDQ7QlbgotR6Ng8KpZ9TktzoBNVD+6CgwxUjz6L20MfizNULyJ5r AyS2LNt23h4OU2eTYYInJVd3tAYmVBDInJBPi25KWQAJlz5XIar3cl7sBpzuyql4m0ikSHtJ7fKt znvvedrSA16ZJwAjGYi235TFMPl41RXRhH5hTlR/b5anxh6xevEwavA1AxdAsX8w1oIdqeuu9lth SWzH3fixsPlsoHiaU3NM2KtchHd99tonRfPRsGopJ3BSRkbjphR1DxRYUDUulnkp95Cmcm273EiA uWUitbmq428RbsM4YhgGyYuj4+JI3pUEYSFSEjsP5IRAhNE55NMZd//7DZ9+/R28rHAk+XaoGaOW 4SJtr60anXoO23eiBxiPCbfnCL6vqUZ/64J4W2x93mvnW1eHtGVPU9Gvs/xWg2h92WRsbRy+Vtnb CTdHNydvEd7ejuKRe2zOv7TDqICDbRdvi0cz2PbZkyztIsMAsQYSNdM2V+03kCFARhIycVqRaEU8 A/PDPc7hHn7ymN6/h5sCjocbjIcJPgRpGxESSbjsrEIowdQvNQ4Er40LaU7FSDalJLv+GfAapMv7 gBA8ih6fNs/WPj33rIP9dvFycFYBoHEodDcpOVpQhJG56CZNsZ65t41wVn9tgPRDzhIzIaUCNkCW 3hvwg1eXVwfvLe19ZTfsGTvZ2UuDalRchJ+xBY1hKVER3FC2g7LQCRcz0hASVQ8n8zgovgg6z1PO FowWZvcAZRFY2yqMAqEi1+YW9jgGLnTALOiXcwQkDXCu6qv6irDW2QDHBqSBJU5Mylnz8YjnR84b FZDNB2XODOhV9+ONMXbLIur5uQEnVh+IME0ThhZUwIzARUWSc82rw6w2LBA1SAABy4KHPz7g4/9+ RXy4R9BnjSnKu5AFZFNz3wLAyuj1thb9QPdrBHC53j2lmnusfE3W4TG7kLdSntPG7+0ZysagAU/h JQ/xpezCk0j4yeq/FHy8PjtyTVcuBzf3v2xR963NC2HCrv2+3Yl3aheQ7ODKtVyzLmo7L0UH6X1z WdC9LQD6T6JWyqTxFpyKBMgxM+IaEdeEtBDiaYYbA9JR4mYM0wQ/TgjjABcC4GXBTiWtvOjSRSAR 4CW6IaeMZAs+RI2Sc0bMErPCpQzvE0JU+wyqbqPGeLQLWaHEt/23HS5LrGZHhTaQSKZO4mBkdkjZ 73qfmFdipwLasAgWg8AcMNgwHgGZowrEaoxq/9q2t8fL89h1aACU7ldtXpnaJJt3UvFMZsnaank4 Nv1iXH1xFSaqnWcqFUBUIirUGSishAndFlwI4GmqqRilAkI2YCHMACcGEpfQ5wQNsKbsS1Zbhuot YWOg4xOTAFoDFapuouZd2D627VdIkczewirNre/X1rbHax6QUQNgkYKqkm0ZqlYzF2qumYuDc3A5 IS0LHn7/iI//+R+WT5/gYoJzjMQJzlOZVzb+RKrqbOYzoQ+c1m9A6oFr6+jniIxvJSTfujDelj2A UVTz38Gz5Jy7daj9XX57oVpkTy3wHF3S59T/Z5brtLAUvhBHT5WnmIqOSihvL6lQq1tVqouXjUFD w7bwxmwquslacBvXHeu2Vdw8oQl16xE7gcUwUwnemtpchVicz0irw8PphHMI8KOEGz/e3sAdDgjD AA4O0OyqYmkPsFHrQ5DnCQxfQJHR11m8UIiRsuzWYk6gWIWsdx6OqEQHNWG6FczdCDTgyzXnbed1 pcnF+DAEeYVKzpXMSFENKk2d0qhWSu/qeNXhFVVCyy6UaxqgIQKj1ftT+e6UBfDOKSsBvbYHJZID vtrpuPLJYnrBGa24tWlh5BArYO3nXLMxaACedpZKaKr1bOmX5q0yuxCbsEXoq+wj9Vc1UMCMAhKY c4nEaqCPlV1i8wwywCKNVsNOfczRpoYAACAASURBVDgSz6fStgYEmeqxZcC2c6hlC7yrYfAHdYv1 IUicmpZdUq+jrJFmOaYCmCT3sSzUaY04/fEBn/77P5w+fADWCM/i5QNkOE8KxsVmiro+rmyMvdyV yWh7fn/r9ppr8peqv/9s+fBa5RrAeOtlT5W8OQMAEJ6r57mmp/4ey5PMxhPXP3X8SfBxsevh7lhR 6pSfjVmwH1L3u9r6l4XfNb6M7YJnth6OrL56jtRDZSESXbm5b+piqsEWjAgX+RLB3FjOZxbqlkXI xhixzgvm0wPO93fAMGCcDvCHAeFwxHCYdEenun9wiXVgkRLl5sJOBGMw1JNFIofKQh2VRvZ5LcZ3 Zqhpn3sA40J/2Lz0e7YanS67OebIAw4I/pItaRkO80oxQdeCP2auqpJmHpggZCizod/RPEO7UwZR SRBnLr5EMk+cM+HdZ+k18MKQoFKdQXE7TQkleRrLheV3ECRxGlFjN2KsSWP4WHb3PX6uDIbNu0rR V+PRDGYxuETSqKucxcuDM3I249oK+NCMWcn4azt8a4BG4yRiZAW85inUMWCb+WAGvKT9HLxHMG+n EGrm4CbPjo07gQpoThqBlbLNJ2krZwncdfr4Cb//5z84/foBflkFeBszQ/ouUFbWicp7bnYYHUBt 5q71s7FAhuPaZ97TsT+nXNuJf46txsV79wQweSuy6THV+Vtp40vLtTEom0zacUX93h9aypc8w8sR 9fX7X2cqRD5c7qL3bDgaMbc5vwvkXJOYXTTBcqDY+b0utuzHCmfKdfFH085W4FrbuKlB2+9VSGYG ckxY8xm8zFjuH4Dg4ccBw2HCdLjB8TjhcLiBHwdEJ2GcK3uCel8iCeDFGcwkYANcGALOGbyKeidl yTeCVXb7YpshhnRV1WA7ViqCo1oRNP1o6GnTtft6R2x+E9uJCsoHiWyqQa6y7qxzNhWQpXQXewUL UlU8SbiJ78EWpEo6ysKvy9iYASHVfDPgwmQANbaIeLP44l2RXYaDl9DykFTuEmZK5xDVWSlVaURS IiSHChDLrrmqJ6Rf6rxhQFRijeC2IHBoABr0WUu0Uf0HDetuKeibuGjaQrT7+M6cmvv/yD0CiiqE CGWOXDPGM+Bq/0Yf4NVTiFwFueY6nnIqgMTsiizGChgYvC9qH+cIcY349McHfPzv/3D/+x+gtTHc tPeVZdYYEFVx3KhDWkDVtr9uHv4uzyufIyNbQPRccPTWy3NtRkL75Tnl+wAfX9JGBuCfPOvx6x8H F50Cg3qAsV3AWmaj6kkrkyH08wZ87DFPIIkS2WwTCa6b6MXQr1xk36+BI+rOq08v7ENJrpakRxki AFJKgGYBTQ8npPCAOA1YpiP8OCC8fwcaK41sbcsmP4jLrrwIOgYci1GeU0HeBsZiZqwpgSgjqqsr UZ9Qzdxcs6Sn6tiAdrdZafT93dQ20u0WbIgKg4WZ4Qo6zFYgxqxyUjNiZlabF+vfXECFxWqwUS2A zO5pUVozgzkp8PNF4DJREyRMQZZjZMcgOBlbkrgMVr/EYFCgo8BEkruJzUsiICmwqJp+BT8NUK7z FQVEdOcoGOHmGGmfU84gi5KZsgKRJGAA/mIB7+6Xc/d7mcakoJYIRGxhQWoxpkPnTgVl8q+wFTo/ LER8uxgXSMC9nQ5Y7JwcEVxmeGWY0rri/vcP+PCf/+D04RMopjYdm71tpb/EnsLsLCr4l8fkDlg9 B1h8H+v965YvVdu05WIefufAoi1PPUsAPo+i+iuU5zzrtY5tKd7m1+3VF8evGfPso10uH0bj9+dc 3s925f3mu89tUuqxc4yxaAQENedB1R8Xd3Mm8Ll0SNVqk7HDmgk6Iq8J5/MZC92Bvcdwfw93mHA4 HDS3SYAfJMxz0KiGmZUC12YyqaB1Ho58B9JagLGnojCBX2w0tC5vKdkLnawqm8ZoyQxaje5nrgKp VXdY/9kgMCrQ6Ch3ZgQxOamCtWk7A4gx1n43tqWwG7X+VujAdrB2P0YzqtzaDIsnEDXjul0gzYYC KPYVhQ1xog5h19hXdAC2L8yS/dfmShG+ipk7RyvtT0+uRPJ0pLEdUgJY5gayqERoZz7vvrXaaRUy ixBu1QvkJDAXE+BdDf9ubsRw1IFOe9b2fmYIW0CF2hGBzIBaVCACDhLW04IPv/+O3/7zX5w+fQKl BMpJAtGxBqqzOaftBSszQ/Xe1z63XfAcsdeqSZ9TvobceE3h/5J7Az8WSHhJec7zv1FX1B+tXAcX u/YtF1dv3X70U3/L6HdHF+OuK7RZjefmPNvJWn2tUKqRQ5vdpgqm7rm4LstQoZBNscCAOSOabDU8 5FFDe5sqJ6eE0+8fwMHjPAzwgwQZOhyPONzeiudJCGU1JGjCKao68m1uF4vCaf3dgoz2Xws0hF1I CCEgUG+vkbWfLEdG7XsVY5v+7/eONciMET7bFzW4ypxt2w0AWVPO73mqtDYBFZD0CyIpq9C2q/3L KSbcgsxyfsxt+gxhO1Q9UpgNU8co4DCG6XJyc9dlDBaAoo1ha3DzmXTMRV3hlCljsORGByhfhFAv l++8imySWOv3zoH8ZUh3ARkEy3HilNGR9vfAojBBzThYbBRTWxloMdsLC1kWiLA8nPHHf3/FH7/+ ivPdnUTkBBCThmW/fIzSBgKL2snmctO/RJdDYOqmy5/3NzpPlc8FIC8pX3uz+yUAZk9Veu34j1Cu Aa5dV9QfyfDka5W9yfelfdXudD9rYpeX2TWAYYfpMJBC2/b2rq45syGP3Wfco/rKBNNdqKPmWLvI 2jeWk4vo08XQQShvLACvCSsB0Xss9yecPt3DhYDD7Q3cGDCME/w0wDuvi7yoTsjCPW9KAQeaP8Q1 BnZbRgNAYTwiqEQIFdBRg5ZZve1nAUzNS7f3ThkgMTFkfVGZkd741K6zSKIFbLTAogEXF6CD6063 0vTcyQEbXyJXE27ZcKEK+20kmdziqphVTlONOWFGpFR/M3HYSjt7Xr9hLbZh5gwEOyLA+6Z1qeIE bq+w+pt+17ZUlZioe8LglKmAMhKNtw0BOkubsOiQRGfkNWOxjT0Xt+Qu8Bqz3K90ugl9AQYPnz7h w//+hw//+xXLwwNczPAsthK+Zaoo2wsnTAtXe5IKNJ4AFtj/7e9yWdr1/Tk2B3+10sqGzubCyl+d 7nmN8vTEehzZtnUYHS9X7aNgi7wnfIHr4lp09zMGwxgDMnYha+1uR9ioyylV3f4eUm0p6C5oA3N1 tbNnaLLeqq+HNk+iW5q3iezOAeaEFM+I5zOyAx7uPoox6HjAcJwkyuE0SlbMEOD8gNy0d6u62YIB 0vZ4iD6ixEnIXD4TGDlFgCWAVzGTI9qEhabC8PQg4nLc5GUsjQJs958aJqg5v/27BR5mSNjeT7Gh 7HKV5cicYMAnNcCkBULMGnHTWJVG/QK5uhiUFt1F7Vg5rgaWrNQDo4ILJoC8K3Eu5BGaFOzKAhgD djmLTWRyoV8cJGdLga4cy5wyoS3RNXtwSAp2hJmo4MKbpwh6UEXWDQ3oKu+FggmLOsvI5T3ktv2k bswAoDFcvLIL8/mM092dGG7+9jvSeYbPDJcZnDRZnFPQUmG5YDUWJk3hcgUVm6n3HGM8GbOXsRZ/ l1qubSp+hPKceVTAxV+Bvnl+eeq59/hVu67dKzxefWEZiqC6PDUXAVTvaXcpTbFfbTW52vzKjEh2 VMngaJ+QTVClt9kj63Hxt6d+S2gnl2aYYK0gJDMkKQSanag2kbS9rT27LNSWDEt2d8RmjwJQBuLD A+IcsNADKHiEYcB4mHA4HhGmCf5wAIUgabi9hlWmstft0p0b51OegQAEV9pCEO8aZi7hoaN5J+iF ZpMhu1FGoKH2z2Zgu/fMUD7qbhoQDwHaXmftLOxE3+9lhAtQyCB4OEfwJDtt5oapUfCRCRKm3Aaf Jd6CZQWtAEXBCBicGE7nT1a7Hct307af9XeCePdIeG6N71AtDkEGLhRgFUapefomWwoq3Jb+NANK Jt26O0gcFk165sjBKwPRRzvVexpAKH/XWUGbdpTvhr/MMIQgQa8UkGbIM1pcOCKn3iekVskKLEAg zljPZ3z67Tf8/t//Ybm7B8+rGHYS1PjX5hNXg2MWDqcEGFP00zJo3d/2TDr211epvW1MPfb0+vg6 pbWbegub3ue24Vp72139j1O2K2hloj/bW+RHL91u8koxULCvUgI6Pnf3+uZ4o58s1TU7ezMW7LUc 7cK8qZO3FuG1nuabLkJyb+L62VgTgJCL8ZjULsZr3a6mYzHkrKSJrkGuj49gbW22g852oLDzGKUq kWyGhQAIXU62OgLgdUVaV8ynE9aPn0BDgJsmhOmA6XjEdJjgLFhXcCCNRcEgNUwE2CJVQu5rNhVm G+FIKWxHBfSAuUm8Jv2W1Bsjz7r4q93B1uixLDKbMbI+lCBWVRi0Q8dgZVoq0CyMlLIonFU1UCKj tGOoizbVT4vvUG7kPfoh5vJZAI7ePEOACDdApADMogZAs8/mQmppYxoGQIU7WRyOcsomAzBVkFDw m82hDELQXDNSt3nUlPobjN3uBQjQWC6sgGDr5UEFVAijVZPUlXPY5pbFbYHSCiRggzM8CahwAPKy 4PzxIz7+/gfufv8dy8M9KGrwOIbEuDCwSLU6Yi7PVBSP1m5UgFR7LZcxtHkNrh5IHQPFe+CCm8/L de9b2EB8q7r2QMBLVeDfg1y9bGNZgLE31vW94M059d3OeLO5Rf6c8j1MBCtb1URrzMlba6/Lq8un xaeon1xiKLDukEDNYtOCKlvsymSzhBLNLhwQF8X2tmg3fLRBTs05VEVSqRYmLKrAS2qFn2IE4or1 4QEIA8IQEDQyosTTOMKPI6bjjRgDcjUELY/X7mL1ebcrrTMjPIcqkLmdP1yELZDBJRy4Xu99FagN 6Ch9Wc7VftxOy61AlcpKM6V/nIKyqn5pWak6RCJd7E97vuduTndVPxsg3AGSR4rZNGTGxbi0DBkD xZjSwF1V22ksD6A8F/QXa4fTUKi5Eag2DzuQxfW+wsJJ7M4YVUWhoMcZyifzkPGdXQ4rS0EQrUYA gWPE6f4e97//gU+//YbTx09I8wIP03xw/Wdwpnntivqt+bvt/tpb9iDtkOhaUeYs1758tOwLm69V /qz1+K9sT/EcdqVd54h2gJfOk7/BxZ9QXoJ6r1FtW3Rd6fXn1X9NJ7g1zrlkW3b3LwAanbKWuohX Wh6oYKFmMb1sd8dSlAolWyo7sTNwRp9Cdp6BnORmSBHr+Sw7+uARxgN8CDjc3sIPgySPmkb4MNTY DZnAFvAql/yaZScrFH8FdoV0adZdskijJhwsf4gKvajeApa/ReJOEEizfTlPHRgwAXLZR21/VuFd vH7auYGa0TNyLr91gM/mD18uGG3Z29F157vnzb9tXbWttVnX2tGxBfpHN5f1TwlCZme2x9SHSUFM a6DrQDWmSGYk1giaXL2KoHPKoqGa/YZ4ZwjC5wRV2yjgy6Jai8uC06c7fPj1V9z/8TvWhzMoMQKj 5EsxsM4F+PfPYP1EVCPu1t+rqvFaH5dxLv33HGHy1ypvQRXzZ5Rr4325kW3f1Mtz/wYXLyhPo7qv Oym3979Am9ud7WfUBbQbWt6cV/XrW1e7xwyX6lejVF6nf0zoVlfLEmCi+PwXw7fMyPGM7Ajz/T18 GBHGAdM0IUySoTKEERQcwjSCnOSGUJcBJItqqrQ/XPW8MfCBEu3TGAfdXbraB8SQuBXg4tVSDEf1 JNcISgM0RLXfDLyVKJdbAZ9N5Nrvdv9Gd99Q/hcgknTsd6b5s8TM3px6hFa+mIPtTnwHVHdnGzNj lwKVdaqEAkpKeRk8mPqgMBPQIGkaOdWCvVkCMVGVqbrMa7ZaXxmK9n6mhnEkeV4cEThFzOcz5ocH 3H34gNPHjzh9+oR0PiMwYXAeDoSUo0afBTpVBYybatQh2vwLLyXsqTX2+r6qqtrf/6pC9anyI4Ks x1Q/10oFFZfzxtZE4O84F2+2XNttPMfw1pbml0wcQNc05goFygKsRntFXdHHY7hGqdmdjcGgTQTU azuzpvGdAHHel/OrNb6wBARIDgoyIz+5KjMDKSInUamkJSKeCTMRyHkMY8A4HOAGh/FwhBu8BPAa ApwPJdpiJjLTC2lXrv1icRzacEy2o7b3kQggZVxIbTecUtLWb5lTEdDG3LTcjoMYqTrKKB4qrhqh FiFY8J8YO0ookfykoKdiULk/B83g8lp5au5eA8fPXtxQ51RJ0IbKPpiXStf8Zv5o7+j8bgKqlay2 EUhNdFcSVZELHt45hKFhurRu6H2FSVDbIAYoRizzgoe7T/j04QPOd/c439+D1xUuZwxwElk2rshZ jTPVFmrb/aSAiORmXT/UfmX9/+O6UTu3vX7PA+Ctqia+VRv+ikDraU8Q8zDcf5cz5N35S4KLvUnz 0sm8j/a/9MX4sgm916YLdLm940X7udmBqpEdaCOI8u49Lst252khtrb3vf7crWCqDI2JiXqpuPZV 4WjrtMUDACyqo2XMZDAS1nVGohlwjIcwAMGXyKBhHDBME8bDUdxdhwDyBPIBDEldnssD4XL4rZ0m LzY6SwAbgd1XUtgGFRxJc24kmHzrmQ1w1nQgVCJLCoKxCxpmoErc0s9XafSdv/fK9uiFAeu1BfuZ C/l2LldsavYlTjygDHAyqooKFoiqyZyq4CLmpInwLO6rBOryXuZCGIIGUyt7tspIKchzRgenhPW8 4Hy6x/3HT7j7+BHnuwfkdQFShoeE+CZ7z/I2L8hl/1l4EBnq3g4KEFBrHiRP9ODVI08J07+awH0L IOdrlc99ttaI89oaIe9TRkzxrwkuvn55YvF9glF4njHNM1pxBWRc26G09CpgXiR1QbvcCxXRXr4B VQfcPKX8t/zQJr9qd+j7i5a1g5UGtiBX5Tg1sQ+aBd+eiZRtIEdwLEG0QFSCJLEmzUKO4MxY1xVw hEiSL4OCU3fXG0l6dhjUZuMAPw4g54Umdw4SgjpvImBqW9hYFLkfkxnAUoeQWpWTPVYbb0JSdksP ZxWaBXQZEMnmhZFL2OqSj6WMEZX+uQAne+PwXOG//X6FqXhpsaRuxGoT0/QNWMKJF/ZNBbcBSYCR UlbVQ+23bNeDwU762FyZg2Y2dWRB6tQVmTS2hPYpR/EWWpYVy/0D7j5+wsP9HZbTjLgsQEwIzOqL pcHi7D0jYwSV8SjqtYq5LOyWo/a9692i98q+mvLpdeStCNZvra75MV1G+/LUs+1vGg1c9GxjeYcs 0nFOSPwXZS6A7xyBf/acv1xcTC8r69pm+4rH6NGGoifXn3ftpdyJLVxtU9prct1T7w6NLa49oDFV AHSHSp2Qri2WlwBFRVF2nZAYDM5BPFuUCoeqK9ISsS4R63mRZ/YkaeLHCeM4IYxB/h5G+MGDgi+R H0sbFEDUnWdNGMb2/GS0YvU2KfYWzS7dWV3q52vjWECVes9AhaYFymp70QJVFYDR9Lm3IbAFRNtb z6P+t8175Epf7wuF9pqr9kPNJXYvG2xvKJMZnMRANuVU2AleUbKsFvCVhcrlXNU+xgQZ8JIU8Q7Z MeAJwxAwhKFmztXrHeo4Ou3vuKyYz2cs8yyqj7t7nB9OiOui8SwASw0vn+bBw6ourP3gYK9MnTPV DqUBHA2zWK/ff2/7fn6Z0Gzr+pZr59M0/euWazZk36282JSnmevdq2CMxeU7XzP7ppTK+vWngour 9P1n0qNWnkKcz1WFPHcibzv5JYO296Luxfn/rPZtVOKXO8nNjpk25zVt6oSECWk7f8vA2OKzuZ9p t7f0uAnHvZYSRNXQ9g3tMRsNuyJBmlTobE81wbsVWs0JtvOvWIu7e3LKotJJQFoj0mnGLIhAmI1R ooS6cYAbPYJmdg0uSOTQIInQTD1j9qdZpSYrL17AQtkpuM0rwbXxDQBwUEbDq8GnCs5OyCoAM7Yj q1A2Q0Bmiz1S58De5zbNeBv10lx1HwMX5ZO5mwFEqmLSPimsSgdspJNySsjzjLxmLHFFXCNyTnDZ l3Gz/zq9lh1q3zHUU0nmoXMe5B0wODXaVLYCYuDpVO3gFYCmGDEvC5bzjIeHe9zf3WN+OCEtq7AS zPBQMKJ9XlkG7fMm8zCbqoMb9qKZq626g7m6t1pvmQ5QceoOsLgG8p8u39oWY49tfao8BQKuHX9K DfgWQcUWcL32eFz2udTfpkywz5QjUopIKdV20Bv1Fvkcyuatl6+B8p8ELJ95/SVSf9yro3X93Lvb lgep9daXoloc713ZtKkwHLXGbtFtn0VZgHwFnXWA4yLlZt+K1siUmJSKtxdKZbpKAs5JBM3pjMUR IgE0BPFAGQf9FK8UrwG94B1ccCA1FrUtNCMrM2OCxgS9tY1K8+Wsuvs2LGLhzMuuFxLFstITTXCr oh4wMAPkdVXdDQrdaZ9tv7fgIoTQJ/lyvTeFAYIWkBf2pN2AkxiEmTVNBSJ6RRkyAitg88OAMM84 n8+Y5xk5cdn5izCXOKIOrqqUFIBqCjQ47+CDhw8ebgy1XZzgWOtJGZwz1pSxzjNO9/c43d1hfjhj Xc6IUbKzlgBebHOGhUmxv20XaCBdSZkyhkQdcCiIw0avvJ8tGLeN2bW39rIIy1e+NUeetwl7aXls TbzGGrxG+dYMyNcsexvr13o22+TUFb6CMvtF7JWaxI8NqLBWvElw8VT5UsbhrZW3oKLZZ122Jxkz YMxC3Z22z1DjV/R1E/m6oLHtym09bSgUu38j4NvPavy4Ucs0KpsngqRub7D5edOOwiCo8NXEZ61X ju1MORGcJ9nZLiviA3B24pFCwcOFgDBNKhTVYHQY4IMH+UGudSIECx2PDLC7YJjIhBCrPYypQ1KB HfUZtH/J9PpQWl5/l120Rq+kcROwsepTW2DIjBL3YVmWOs7ew/tQgIcBEWM7QgiFkeh35/I/51yX dbUDsps54ULA5L0a3Y4Y5xnn04w1SpwTTlkNIQW0qA5KmB5n7VKA5IPkGIGEQGfdiaWckWPEOi+I 84yHu3uss9hRpHlBTpIGPRgYUkAjc6mfO+3fFVdxB2jFjgOb6wxQatCU+oMaClfWw/qsfu7vzHtg cR3of43yZ615r2XI/xbLl4CyC1DXrO22KWA9j1mMNkUVEjuWsi1vElw8R5XwFko7II+16TkvUk/D vU77Pqf0Onl9pgaxNmcqSkY5dkm/FlTRfWfkXRBRr+13xhWU9CCi/r1hWFqS40XlegUWN8PaJoIg lfY5oQxKci57AaELf4YYicIFnO8f5IV1knDLD0G8EcYB3nKiDAOGIcCrwHMugFRAMwwICIXCqr4p IKMwFBuRkW1nSw2QQPmtdKUTIU7NMDrvujGX36sdQ1G7gMV7xoJNNcVYixZobD+JCOSptMvYigqO 0GCM6h0yDgOCD5imCcdjxDwveDg9YJ3FO6MG0pLIqo4I3ruNWgdwzEBM4GXFMs9YlhnLeca6zFjP M9Z5RoqxJGcjVX10LFPb8+al1Er+hokjar+jRgpHT+rIWf07cAkh6zGrf69c2l88vnZ9q7JvH/J3 2StfQ23Tgom23j2jzZxT+ftaPW8SXPyI5S0bBV28zA3FZceJZItUztRF8TF95ZZyrdlaL+MktGoS Q8j2+3bRuVw8q43BPnR5btku/ttGts/VtlVtMVTqkYbP1sTolfdgBnKCM9YnRmQw0omwEpXMnOQ9 QvAa2EvYDe/FHdarKqB4eaiXinceJJmxJLW3dF55HFN7MNe2F6NB7MyBiqVkV2y2D3bYVB3KHvWR S1ynTim9q7+t61rr2Pnng2YpBSmD42padGqymkIT+yGB9XhwHmHyGEPA4B2WcMZyXpAaVY/0l8Ng ie3AWJcF87IgLivSvCLNC5Z5wTKfsS4rcooodg5cDTMF6RgYM3VPnU8CBPu+FZZC2wKqqpNmmpV5 3F3aeAXpd9ocbz+fFtLXgcW3EPBfi714C0zwtyrf6jnt3ZXYL6mz1brGmLxJcPE9IdeX0E9fes5r Tqj9e9nihKIX3wcR1qCn6ycisdSnfQajE0Lod8eFobAEY3nblly2eSIvHa5oPB5vruoZidHR8rvP 1BoYkpDSzOKV4Jgu4FMGJOAWJ6HhScQJG3hSgQVkrHlGAmFRYcuk8RaGAc4HjNMIOAfnPFxhPQKC G+CCB0LQ3BwqnGWrXuJ7MFSAmX5Un3fLTDxWHlNNGtNQmAitrwUbW4Nh2wExM+K6AtHmnrkMO81T o+nTva9eM6pKEYBF4JzhHOHGDzgcPBZ4nBlY5xk5JjgNnDXnEx5iQowr5vmMZZkR5xV5SaBkWYIz wFm8PXQcUlzBOXdAVp4ZBfCY2u+aes7AkWMji+S7aVVK2Pnas1vMv7WM2N357+nmHysX7OM3Kt/a ruN7L4+N45c+XzvzWrbCvEHE5ufpzfKbBBfPLT+y/uyx8uRzfrFayXa2KrGv9LMJ8G0kz717GI0r lLwthO7iuot7kOzsRABfiwzZuJ9y1yJ9lr7+ywiT9Xw2m5Km+RcCwgQjVeEhxIsCsZQKQDIA4qx+ ADmugOrVuRGQ1g2ezJ5DYnAk1sRsa0IkYP4kXUEkhqHeecmd4r2oTgr4cEXN4oMZWSo7Uv5pdE/S 9oAuAAChAZJNn5eebZkHUMkqa5SJAQjjekh/L8CDapr1wgq1tD8DbEGmCMiZwS4aVyTnWgZaZiAl ORdQ25eI5XzC+XRCnGcQAylJ5Mx1XpCTeJnY07ps4MuV3xgZzBk5Caiw2Ba1J7gAtzrdlJWgJkR9 w0o4Fk8SxxVkECz3Czf1cHl/2v7vxuIVVAp/xvr5uSzD55z/V2IwXnXsuMILZi6Rau3TGK+99b7d NLw9cEGfg65/vCKb4mvPt+2XK+c92T3PBSdVOPSHe3VIEezPuCtzh4t1p3YNBW/VLtt2Xwczcq66 EV745l55fkLxDLHrUa6v6mrUbwAACAVJREFUIKVlAEpdgiZEQHSCst5TNrN9+GwRgKz3lhc2JmmB c74GJNPdbOaa1ZNzBCcgMSESI5paBAY+1KYgCPgotg3DIKoF50UF4YIkS3NB0m44XxgP55T1gIEg HZdGRdYCDLSMFDnA8mSAtT4qKhozRCz0if7piUtmUG7+iby181jSkZtvfbLQ3RlICWldsa4ROa7g yEhpxbquoh5hBjgXw13vahI56PsnKolmh3ahivt/7V3bduMgDBwl+/8/vDHaB6ErYDvtpkna6pye thiDuBiGkQAOagtzZoeUDXi835aqgKGXo1saDYB4qiz9Lm3VHr+Dah7M3+QMVsNAm7WbQriMikp+ j5Wz4/kZRvdMWt9h18jnyxB7x5iO+lM5Y6Gs45qx0MWCLiJeAlzkVaqsfCKdukfBfiSfVxYp8lzP 2p7L8hx9hAc62BXpafVUdRkHIuuqi+yblsuebwDGQ5ho8pf8q91W01kwGXYokS1fS3qLexe432YK tZP3ExTLIUfJpyRVQ7aBZ4zo5zW02r6hzdkAF9DaNkS7qv6MMt0wLiy6XwHI9SQb+O9fbJCalhNH EYDAFZcrCYi5iplFnE7dt8HAhf4dzC3KOKRvlQh8vXZGZjIAwVfxsT6tLzODtpuBCzlZsx/T3s+O UCDBrclZF+EekNYa+Lb1nTMMNJ04BeSoH4zWGoHBWwwLjVrbRfVn3TUDY500WWK23SnGwmgKZfJX lq2Rwg+EvAqomJg64mCf69h/zowEM1bqK8HF/5TsCJ4XH9+JxThj7lo5fapZw/D0ZGGmfhV2v84i rRrmTqEves7Fr8zlVT6QGajJSHquY8XJsUPOB4X99NbP44Ae/185kmqZWommg7geAlZLUgHEOuwe URNJTSaBrs746NSnF17ZNmCuUziECejIRZxrN2wE3DTVvuqIZiDu4bg4MxHNGaSOBsFMQJc/sBPC Qtquxggu7DcxuG9v6xSHgAvuQEEBQ8vH08e0LpjnRcg7Oayd6kBtTILoMLQDeRtVIRITh6STz6MY V5s8+Sv+NzIS98g7LKa+Qlb97WfK2BfjGLj1025v7WZhMc498nBw8RnlqmPSK0ysz5Y6Eb+KnGln G8wvY5w9x9IZKIjP6y2r67pZ111cBbKe7jhSRRLOkuujJG0LXQnDgIKrVyfJFF3CUpy+vmWdTHtI 6/wN++TKAUi0FhgtyuvcC4A/3Hd6cD91svw2e4KurdnZjAYGXwXgRLOJ+hww64VfCHprOjDgNKuT zFocSGEZtBaVlZiJW4TUOXcWJ0JEp58qqzHTvyiIvbK82vjwCvKd5pCPta/0mVkVKLC43W64tRta uZSy7trbdSjtcZ7GXKwG+CMv2O/SOT4jrzxwqG0cyKz4vsrrNfqZ51wGct06Ocsng6DFHSrDgF9W qYkNwbBK/zylfEBmGxDIPiv2dvpOPC66yRFAOOHTYzlfFELVlBLybaG4zN3c1RMjAFcWEw21HXCh 4IEUwPSTUElO09yoTuGqLaVbbwH3jPG52oFL7Qbq9Bk5g1rufAKs16HmhRwUhO3CNH+/9LH0twZV hmIEDpXxqAsvzWvI40BeeSxZye888H9E+2Z02Ny223T8TL5PpxJ/MHPxEebh/br6c+UZH9rKF8bC wxDvD9fpjekMMUr8qs9cv0lOKU+iOeMxrjovyFNdUYAb9gpYUxugUw04QVwAgYWo9ZcmN40iE65Y MPy0z8bhAKimkytglgGlJoQ2ACMQT51Z0PjKLuhJm+6rQum3gxr9X5AGQe7v4OYMAYcyuLVgy+Uk vTvEy1kn4Fohbh7hVN9eBg1zneV1BQuTtBmoNrHESHAb2moEFpNkh3fmz5Iv0I48GlB8BWD5NRWd kzo/1PLHLaZb2yCXR8IWTDNAcWa+IX7SVtTdBma2QeLIofOnycxGpr+fgeSrc54MzHOHqiSq9yrh o/be6z5A0irrsM94eHjVv3xgFQ1QSXl8POi4L4fTwzx2mDjnXI+bFGR6ZFuJc6D79eTOuNmyQc0U QQM1mdRcCNgURBgz0duFvH2osyIe3vPt54TMbr/VYopPSQQKuQ78vaidsBZ7tUtECUpqvvYOc+31 DrJYAE/s3clyVZgKDwh/h/C1r8AeIOFUV8PTIa/3kVm/Xsb9ZTYQv1axIHq/kZ+GTbeYbhvUmRzF BBKlXly2Jw8DFx8xe3w03Z8gX+mUdOSQaAPvdKAOOyYWTV0XouPzygyUhAooSD4XJKBhThGfM1+M 71ZQV/UtaS13v/TUDsDHClxxqTgHFUgPaAijtPxn3R5a6Hs/jUOTmq1aJIYt0hOwEvPRdhHThjIB ifzo5YuMROQ1AODSSA4iY5jvRQv9ijRBW6lHEGKUiwymCSjmOzws99kJmpZgBAA5TtyVNL7T7L3Y vqOJZGQbjnyClqL6LKK9y9i5Gn+41OVKZj43P9eUYl8ibKQyU8iGrd3sG43j+qwJ7l3s/+4WeQN5 NW/nGRW7iDhMtOmd0x/7fnkjmNDLTud6HRkoVqHlo4ppExnWUdQv51ZQfT0ovJ9hBVf3rtao68jT raCi4xTg6WTf67BOfrpDhNm3Sg4TQd+xYiyDDmwFEDmAAXTDiU4eBIeBfkx21LXnHRyDqW+vueQY 3gydibG6iTqnpb4fyGbARFf7HCLT5LnJHiPGznTUdh76rE6Mq7Rqvu8BHo5kxbzc48hewcR3Ahbn x3+vA6271hq2JiYQPcJbn8/S3jOpHMk/gKxVTQOMbRgAAAAASUVORK5CYII= " - id="image1-7" - x="144.39027" - y="145.30473" /><text - xml:space="preserve" - style="font-size:8.94777px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#000000;stroke-width:0.760912" - x="59.22551" - y="99.922089" - id="text48"><tspan - sodipodi:role="line" - style="font-weight:bold;font-size:8.94777px;fill:#000000;stroke-width:0.760912" - x="59.22551" - y="99.922089" - id="tspan49">SPQlios API</tspan></text><rect - style="fill:none;stroke:#000000;stroke-width:1.3;stroke-dasharray:2.60000398,2.60000398;stroke-dashoffset:0" - id="rect50" - width="141.88351" - height="45.035332" - x="17.291592" - y="75.534477" - rx="5" - ry="5" /><path - style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)" - d="m 38.945222,144.01501 c 15.201046,0.99574 24.999562,-2.65083 33.799115,-15.25884" - id="path50" - sodipodi:nodetypes="cc" /><path - style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)" - d="m 75.967274,146.51446 c 2.925331,-3.24509 6.283122,-11.54669 7.360403,-16.17079" - id="path51" - sodipodi:nodetypes="cc" /><path - style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)" - d="m 138.52655,144.01501 c -15.20113,0.99574 -24.9997,-2.65083 -33.79931,-15.25884" - id="path53" - sodipodi:nodetypes="cc" /><path - style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)" - d="m 102.3458,146.51446 c -2.925344,-3.24509 -6.283132,-11.54669 -7.360413,-16.17079" - id="path54" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.5;stroke-dasharray:none;marker-end:url(#ConcaveTriangle)" - d="m 68.985665,40.655103 h 5.984828" - id="path55" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.5;stroke-dasharray:none;marker-end:url(#ConcaveTriangle)" - d="M 77.263825,44.359272 H 71.278997" - id="path56" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.5;stroke-dasharray:none;marker-end:url(#ConcaveTriangle)" - d="m 105.49819,40.655103 h 5.98483" - id="path57" - sodipodi:nodetypes="cc" /><path - style="fill:none;stroke:#666666;stroke-width:0.5;stroke-dasharray:none;marker-end:url(#ConcaveTriangle)" - d="m 114.30551,44.359272 h -5.98483" - id="path58" - sodipodi:nodetypes="cc" /><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="91.075272" - y="40.35569" - id="text2"><tspan - sodipodi:role="line" - id="tspan1" - style="font-size:3.52778px;text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="91.075272" - y="40.35569">Chimera</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;text-align:center;text-anchor:middle;fill:#666666;stroke-width:0.3" - x="91.075272" - y="46.352917" - id="tspan2">SchemeSwitch</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';fill:#666666;stroke-width:0.3" - x="150.57195" - y="40.35569" - id="text4"><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="150.57195" - y="40.35569" - id="tspan3">LWE/SIS</tspan><tspan - sodipodi:role="line" - style="font-size:3.52778px;fill:#666666;stroke-width:0.3" - x="150.57195" - y="46.352917" - id="tspan4">Frodo, FrodoPIR</tspan></text><text - xml:space="preserve" - style="font-size:3.52777px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';text-align:center;text-anchor:middle;fill:#ff0000;stroke:#ff0000;stroke-width:1" - x="73.55233" - y="22.175295" - id="text5"><tspan - sodipodi:role="line" - id="tspan5" - style="stroke-width:1" - x="73.55233" - y="22.175295" /></text><text - xml:space="preserve" - style="font-size:8.89214px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';text-align:start;text-anchor:start;fill:#000000;stroke-width:0.756182" - x="65.787498" - y="19.72295" - id="text7"><tspan - sodipodi:role="line" - style="font-size:8.89214px;text-align:start;text-anchor:start;fill:#000000;stroke-width:0.756182" - x="65.787498" - y="19.72295" - id="tspan7">Potential use-cases</tspan></text><text - xml:space="preserve" - style="font-size:3.52778px;line-height:1.7;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';text-align:start;text-anchor:start;fill:#000000;stroke-width:0.3" - x="66.560791" - y="25.474648" - id="text9"><tspan - sodipodi:role="line" - style="font-style:italic;font-size:3.52778px;text-align:start;text-anchor:start;fill:#000000;stroke-width:0.3" - x="66.560791" - y="25.474648" - id="tspan9">that would benefit from the API</tspan></text></g></svg>
@@ -1,139 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> - -<svg - version="1.1" - id="Layer_1" - x="0px" - y="0px" - viewBox="0 0 270 49.4" - style="enable-background:new 0 0 270 49.4;" - xml:space="preserve" - sodipodi:docname="logo-sandboxaq-black.svg" - inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"><defs - id="defs9839"> - - - - - - - </defs><sodipodi:namedview - id="namedview9837" - pagecolor="#ffffff" - bordercolor="#000000" - borderopacity="0.25" - inkscape:showpageshadow="2" - inkscape:pageopacity="0.0" - inkscape:pagecheckerboard="0" - inkscape:deskcolor="#d1d1d1" - showgrid="false" - inkscape:zoom="1.194332" - inkscape:cx="135.64068" - inkscape:cy="25.118645" - inkscape:window-width="804" - inkscape:window-height="436" - inkscape:window-x="190" - inkscape:window-y="27" - inkscape:window-maximized="0" - inkscape:current-layer="Layer_1" /> -<style - type="text/css" - id="style9786"> - .st0{fill:#EBB028;} - .st1{fill:#FFFFFF;} -</style> -<text - transform="matrix(1 0 0 1 393.832 -491.944)" - class="st1" - style="font-family:'Satoshi-Medium'; font-size:86.2078px;" - id="text9788">SANDBOX </text> -<text - transform="matrix(1 0 0 1 896.332 -491.944)" - class="st1" - style="font-family:'Satoshi-Black'; font-size:86.2078px;" - id="text9790">AQ</text> -<g - id="g9808"> - <g - id="g9800"> - <g - id="g9798"> - <path - class="st0" - d="m 8.9,9.7 v 3.9 l 29.6,17.1 v 2.7 c 0,1.2 -0.6,2.3 -1.6,2.9 L 31,39.8 v -4 L 1.4,18.6 V 15.9 C 1.4,14.7 2,13.6 3.1,13 Z" - id="path9792" /> - <path - class="st0" - d="M 18.3,45.1 3.1,36.3 C 2.1,35.7 1.4,34.6 1.4,33.4 V 26 L 28,41.4 21.5,45.1 c -0.9,0.6 -2.2,0.6 -3.2,0 z" - id="path9794" /> - <path - class="st0" - d="m 21.6,4.3 15.2,8.8 c 1,0.6 1.7,1.7 1.7,2.9 v 7.5 L 11.8,8 18.3,4.3 c 1,-0.6 2.3,-0.6 3.3,0 z" - id="path9796" /> - </g> - </g> - <g - id="g9806"> - <polygon - class="st0" - points="248.1,23.2 248.1,30 251.4,33.8 257.3,33.8 " - id="polygon9802" /> - <path - class="st0" - d="m 246.9,31 -0.1,-0.1 h -0.1 c -0.2,0 -0.4,0 -0.6,0 -3.5,0 -5.7,-2.6 -5.7,-6.7 0,-4.1 2.2,-6.7 5.7,-6.7 3.5,0 5.7,2.6 5.7,6.7 0,0.3 0,0.6 0,0.9 l 3.6,4.2 c 0.7,-1.5 1,-3.2 1,-5.1 0,-6.5 -4.2,-11 -10.3,-11 -6.1,0 -10.3,4.5 -10.3,11 0,6.5 4.2,11 10.3,11 1.2,0 2.3,-0.2 3.4,-0.5 l 0.5,-0.2 z" - id="path9804" /> - </g> - </g><g - id="g9824" - style="fill:#1a1a1a"> - <path - class="st1" - d="m 58.7,13.2 c 4.6,0 7.4,2.5 7.4,6.5 h -4.6 c 0,-1.5 -1.1,-2.4 -2.9,-2.4 -1.9,0 -3.1,0.9 -3.1,2.3 0,1.3 0.7,1.9 2.2,2.2 l 3.2,0.7 c 3.8,0.8 5.6,2.6 5.6,5.9 0,4.1 -3.2,6.8 -8.1,6.8 -4.7,0 -7.8,-2.6 -7.8,-6.5 h 4.6 c 0,1.6 1.1,2.4 3.2,2.4 2.1,0 3.4,-0.8 3.4,-2.2 0,-1.2 -0.5,-1.8 -2,-2.1 l -3.2,-0.7 c -3.8,-0.8 -5.7,-2.9 -5.7,-6.4 0,-3.7 3.2,-6.5 7.8,-6.5 z" - id="path9810" - style="fill:#1a1a1a" /> - <path - class="st1" - d="M 70.4,34.9 78,13.6 h 4.5 l 7.6,21.3 h -4.9 l -1.5,-4.5 h -6.9 l -1.5,4.5 z m 7.7,-8.4 h 4.2 L 80.8,22 c -0.2,-0.7 -0.5,-1.6 -0.6,-2.1 -0.1,0.5 -0.3,1.3 -0.6,2.1 z" - id="path9812" - style="fill:#1a1a1a" /> - <path - class="st1" - d="M 95.3,34.9 V 13.6 h 4.6 l 9,13.5 V 13.6 h 4.6 v 21.3 h -4.6 l -9,-13.5 v 13.5 z" - id="path9814" - style="fill:#1a1a1a" /> - <path - class="st1" - d="M 120.7,34.9 V 13.6 h 8 c 6.2,0 10.6,4.4 10.6,10.7 0,6.2 -4.2,10.6 -10.3,10.6 z m 4.7,-17 v 12.6 h 3.2 c 3.7,0 5.8,-2.3 5.8,-6.3 0,-4 -2.3,-6.4 -6.1,-6.4 h -2.9 z" - id="path9816" - style="fill:#1a1a1a" /> - <path - class="st1" - d="m 145.4,13.6 h 8.8 c 4.3,0 6.9,2.2 6.9,5.9 0,2.3 -1,3.9 -3,4.8 2.1,0.7 3.2,2.3 3.2,4.7 0,3.8 -2.5,5.9 -7.1,5.9 h -8.8 z m 4.7,4.1 v 4.6 h 3.7 c 1.7,0 2.6,-0.8 2.6,-2.4 0,-1.5 -0.9,-2.3 -2.6,-2.3 h -3.7 z m 0,8.5 v 4.6 h 3.9 c 1.7,0 2.6,-0.8 2.6,-2.4 0,-1.4 -0.9,-2.2 -2.6,-2.2 z" - id="path9818" - style="fill:#1a1a1a" /> - <path - class="st1" - d="m 176.5,35.2 c -6.1,0 -10.4,-4.5 -10.4,-11 0,-6.5 4.3,-11 10.4,-11 6.2,0 10.4,4.5 10.4,11 0,6.5 -4.2,11 -10.4,11 z m 0.1,-17.5 c -3.4,0 -5.5,2.4 -5.5,6.5 0,4.1 2.1,6.5 5.5,6.5 3.4,0 5.5,-2.5 5.5,-6.5 0,-4 -2.1,-6.5 -5.5,-6.5 z" - id="path9820" - style="fill:#1a1a1a" /> - <path - class="st1" - d="m 190.4,13.6 h 5.5 l 1.8,2.8 c 0.8,1.2 1.5,2.5 2.5,4.3 l 4.3,-7 h 5.4 l -6.7,10.6 6.7,10.6 h -5.5 L 203,32.7 c -1.1,-1.7 -1.8,-3 -2.8,-4.9 l -4.6,7.1 h -5.5 l 7.1,-10.6 z" - id="path9822" - style="fill:#1a1a1a" /> - </g><path - class="st0" - d="m 229,34.9 h 4.7 L 226,13.6 h -4.3 L 214,34.8 h 4.6 l 1.6,-4.5 h 7.1 z m -5.1,-14.6 c 0,0 0,0 0,0 0,-0.1 0,-0.1 0,0 l 2.2,6.2 h -4.4 z" - id="path9826" /><g - id="g9832"> - <path - class="st1" - d="m 259.5,11.2 h 3.9 v 1 h -1.3 v 3.1 h -1.3 v -3.1 h -1.3 z m 4.5,0 h 1.7 l 0.6,2.5 0.6,-2.5 h 1.7 v 4.1 h -1 v -3.1 l -0.8,3.1 h -0.9 l -0.8,-3.1 v 3.1 h -1 v -4.1 z" - id="path9830" /> - </g> -</svg>
@@ -1,133 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> - -<svg - version="1.1" - id="Layer_1" - x="0px" - y="0px" - viewBox="0 0 270 49.4" - style="enable-background:new 0 0 270 49.4;" - xml:space="preserve" - sodipodi:docname="logo-sandboxaq-white.svg" - inkscape:version="1.2.2 (1:1.2.2+202212051551+b0a8486541)" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"><defs - id="defs9839" /><sodipodi:namedview - id="namedview9837" - pagecolor="#ffffff" - bordercolor="#000000" - borderopacity="0.25" - inkscape:showpageshadow="2" - inkscape:pageopacity="0.0" - inkscape:pagecheckerboard="0" - inkscape:deskcolor="#d1d1d1" - showgrid="false" - inkscape:zoom="2.3886639" - inkscape:cx="135.22204" - inkscape:cy="25.327967" - inkscape:window-width="1072" - inkscape:window-height="688" - inkscape:window-x="0" - inkscape:window-y="0" - inkscape:window-maximized="1" - inkscape:current-layer="Layer_1" /> -<style - type="text/css" - id="style9786"> - .st0{fill:#EBB028;} - .st1{fill:#FFFFFF;} -</style> -<text - transform="matrix(1 0 0 1 393.832 -491.944)" - class="st1" - style="font-family:'Satoshi-Medium'; font-size:86.2078px;" - id="text9788">SANDBOX </text> -<text - transform="matrix(1 0 0 1 896.332 -491.944)" - class="st1" - style="font-family:'Satoshi-Black'; font-size:86.2078px;" - id="text9790">AQ</text> -<g - id="g9834"> - <g - id="g9828"> - <g - id="g9808"> - <g - id="g9800"> - <g - id="g9798"> - <path - class="st0" - d="M8.9,9.7v3.9l29.6,17.1v2.7c0,1.2-0.6,2.3-1.6,2.9L31,39.8v-4L1.4,18.6v-2.7c0-1.2,0.6-2.3,1.7-2.9 L8.9,9.7z" - id="path9792" /> - <path - class="st0" - d="M18.3,45.1L3.1,36.3c-1-0.6-1.7-1.7-1.7-2.9V26L28,41.4l-6.5,3.7C20.6,45.7,19.3,45.7,18.3,45.1z" - id="path9794" /> - <path - class="st0" - d="M21.6,4.3l15.2,8.8c1,0.6,1.7,1.7,1.7,2.9v7.5L11.8,8l6.5-3.7C19.3,3.7,20.6,3.7,21.6,4.3z" - id="path9796" /> - </g> - </g> - <g - id="g9806"> - <polygon - class="st0" - points="248.1,23.2 248.1,30 251.4,33.8 257.3,33.8 " - id="polygon9802" /> - <path - class="st0" - d="M246.9,31l-0.1-0.1l-0.1,0c-0.2,0-0.4,0-0.6,0c-3.5,0-5.7-2.6-5.7-6.7c0-4.1,2.2-6.7,5.7-6.7 s5.7,2.6,5.7,6.7c0,0.3,0,0.6,0,0.9l3.6,4.2c0.7-1.5,1-3.2,1-5.1c0-6.5-4.2-11-10.3-11c-6.1,0-10.3,4.5-10.3,11s4.2,11,10.3,11 c1.2,0,2.3-0.2,3.4-0.5l0.5-0.2L246.9,31z" - id="path9804" /> - </g> - </g> - <g - id="g9824"> - <path - class="st1" - d="M58.7,13.2c4.6,0,7.4,2.5,7.4,6.5h-4.6c0-1.5-1.1-2.4-2.9-2.4c-1.9,0-3.1,0.9-3.1,2.3c0,1.3,0.7,1.9,2.2,2.2 l3.2,0.7c3.8,0.8,5.6,2.6,5.6,5.9c0,4.1-3.2,6.8-8.1,6.8c-4.7,0-7.8-2.6-7.8-6.5h4.6c0,1.6,1.1,2.4,3.2,2.4 c2.1,0,3.4-0.8,3.4-2.2c0-1.2-0.5-1.8-2-2.1l-3.2-0.7c-3.8-0.8-5.7-2.9-5.7-6.4C50.9,16,54.1,13.2,58.7,13.2z" - id="path9810" /> - <path - class="st1" - d="M70.4,34.9L78,13.6h4.5l7.6,21.3h-4.9l-1.5-4.5h-6.9l-1.5,4.5H70.4z M78.1,26.5h4.2L80.8,22 c-0.2-0.7-0.5-1.6-0.6-2.1c-0.1,0.5-0.3,1.3-0.6,2.1L78.1,26.5z" - id="path9812" /> - <path - class="st1" - d="M95.3,34.9V13.6h4.6l9,13.5V13.6h4.6v21.3h-4.6l-9-13.5v13.5H95.3z" - id="path9814" /> - <path - class="st1" - d="M120.7,34.9V13.6h8c6.2,0,10.6,4.4,10.6,10.7c0,6.2-4.2,10.6-10.3,10.6H120.7z M125.4,17.9v12.6h3.2 c3.7,0,5.8-2.3,5.8-6.3c0-4-2.3-6.4-6.1-6.4H125.4z" - id="path9816" /> - <path - class="st1" - d="M145.4,13.6h8.8c4.3,0,6.9,2.2,6.9,5.9c0,2.3-1,3.9-3,4.8c2.1,0.7,3.2,2.3,3.2,4.7c0,3.8-2.5,5.9-7.1,5.9 h-8.8V13.6z M150.1,17.7v4.6h3.7c1.7,0,2.6-0.8,2.6-2.4c0-1.5-0.9-2.3-2.6-2.3H150.1z M150.1,26.2v4.6h3.9c1.7,0,2.6-0.8,2.6-2.4 c0-1.4-0.9-2.2-2.6-2.2H150.1z" - id="path9818" /> - <path - class="st1" - d="M176.5,35.2c-6.1,0-10.4-4.5-10.4-11s4.3-11,10.4-11c6.2,0,10.4,4.5,10.4,11S182.7,35.2,176.5,35.2z M176.6,17.7c-3.4,0-5.5,2.4-5.5,6.5c0,4.1,2.1,6.5,5.5,6.5c3.4,0,5.5-2.5,5.5-6.5C182.1,20.2,180,17.7,176.6,17.7z" - id="path9820" /> - <path - class="st1" - d="M190.4,13.6h5.5l1.8,2.8c0.8,1.2,1.5,2.5,2.5,4.3l4.3-7h5.4l-6.7,10.6l6.7,10.6h-5.5l-1.4-2.2 c-1.1-1.7-1.8-3-2.8-4.9l-4.6,7.1h-5.5l7.1-10.6L190.4,13.6z" - id="path9822" /> - </g> - <path - class="st0" - d="M229,34.9h4.7L226,13.6h-4.3l-7.7,21.2h4.6l1.6-4.5h7.1L229,34.9z M223.9,20.3 C223.9,20.3,223.9,20.3,223.9,20.3C223.9,20.2,223.9,20.2,223.9,20.3l2.2,6.2h-4.4L223.9,20.3z" - id="path9826" /> - </g> - <g - id="g9832"> - <path - class="st1" - d="M259.5,11.2h3.9v1h-1.3v3.1h-1.3v-3.1h-1.3V11.2L259.5,11.2z M264,11.2h1.7l0.6,2.5l0.6-2.5h1.7v4.1h-1v-3.1 l-0.8,3.1h-0.9l-0.8-3.1v3.1h-1V11.2L264,11.2z" - id="path9830" /> - </g> -</g> -</svg>
@@ -1,2 +0,0 @@
-library: spqlios-fft -version: 2.0.0
@@ -1,27 +0,0 @@
-#!/bin/sh - -# this script generates one tag if there is a version change in manifest.yaml -cd `dirname $0`/.. -if [ "v$1" = "v-y" ]; then - echo "production mode!"; -fi -changes=`git diff HEAD~1..HEAD -- manifest.yaml | grep 'version:'` -oldversion=$(echo "$changes" | grep '^-version:' | cut '-d ' -f2) -version=$(echo "$changes" | grep '^+version:' | cut '-d ' -f2) -echo "Versions: $oldversion --> $version" -if [ "v$oldversion" = "v$version" ]; then - echo "Same version - nothing to do"; exit 0; -fi -if [ "v$1" = "v-y" ]; then - git config user.name github-actions - git config user.email github-actions@github.com - git tag -a "v$version" -m "Version $version" - git push origin "v$version" -else -cat <<EOF - # the script would do: - git tag -a "v$version" -m "Version $version" - git push origin "v$version" -EOF -fi -
@@ -1,102 +0,0 @@
-#!/bin/sh - -# ONLY USE A PREFIX YOU ARE CONFIDENT YOU CAN WIPE OUT ENTIRELY -CI_INSTALL_PREFIX=/opt/spqlios -CI_REPO_URL=https://spq-dav.algonics.net/ci -WORKDIR=`pwd` -if [ "x$DESTDIR" = "x" ]; then - DESTDIR=/ -else - mkdir -p $DESTDIR - DESTDIR=`realpath $DESTDIR` -fi -DIR=`dirname "$0"` -cd $DIR/.. -DIR=`pwd` - -FULL_UNAME=`uname -a | tr '[A-Z]' '[a-z]'` -HOST=`echo $FULL_UNAME | sed 's/ .*//'` -ARCH=none -case "$HOST" in - *linux*) - DISTRIB=`lsb_release -c | awk '{print $2}' | tr '[A-Z]' '[a-z]'` - HOST=linux-$DISTRIB - ;; - *darwin*) - HOST=darwin - ;; - *mingw*|*msys*) - DISTRIB=`echo $MSYSTEM | tr '[A-Z]' '[a-z]'` - HOST=msys64-$DISTRIB - ;; - *) - echo "Host unknown: $HOST"; - exit 1 -esac -case "$FULL_UNAME" in - *x86_64*) - ARCH=x86_64 - ;; - *aarch64*) - ARCH=aarch64 - ;; - *arm64*) - ARCH=arm64 - ;; - *) - echo "Architecture unknown: $FULL_UNAME"; - exit 1 -esac -UNAME="$HOST-$ARCH" -CMH= -if [ -d lib/spqlios/.git ]; then - CMH=`git submodule status | sed 's/\(..........\).*/\1/'` -else - CMH=`git rev-parse HEAD | sed 's/\(..........\).*/\1/'` -fi -FNAME=spqlios-arithmetic-$CMH-$UNAME.tar.gz - -cat <<EOF -================= CI MINI-PACKAGER ================== -Work Dir: WORKDIR=$WORKDIR -Spq Dir: DIR=$DIR -Install Root: DESTDIR=$DESTDIR -Install Prefix: CI_INSTALL_PREFIX=$CI_INSTALL_PREFIX -Archive Name: FNAME=$FNAME -CI WebDav: CI_REPO_URL=$CI_REPO_URL -===================================================== -EOF - -if [ "x$1" = "xcreate" ]; then - rm -rf dist - cmake -B build -S . -DCMAKE_INSTALL_PREFIX="$CI_INSTALL_PREFIX" -DCMAKE_BUILD_TYPE=Release -DENABLE_TESTING=ON -DWARNING_PARANOID=ON -DDEVMODE_INSTALL=ON || exit 1 - cmake --build build || exit 1 - rm -rf "$DIR/dist" 2>/dev/null - rm -f "$DIR/$FNAME" 2>/dev/null - DESTDIR="$DIR/dist" cmake --install build || exit 1 - if [ -d "$DIR/dist$CI_INSTALL_PREFIX" ]; then - tar -C "$DIR/dist" -cvzf "$DIR/$FNAME" . - else - # fix since msys can mess up the paths - REAL_DEST=`find "$DIR/dist" -type d -exec test -d "{}$CI_INSTALL_PREFIX" \; -print` - echo "REAL_DEST: $REAL_DEST" - [ -d "$REAL_DEST$CI_INSTALL_PREFIX" ] && tar -C "$REAL_DEST" -cvzf "$DIR/$FNAME" . - fi - [ -f "$DIR/$FNAME" ] || { echo "failed to create $DIR/$FNAME"; exit 1; } - [ "x$CI_CREDS" = "x" ] && { echo "CI_CREDS is not set: not uploading"; exit 1; } - curl -u "$CI_CREDS" -T "$DIR/$FNAME" "$CI_REPO_URL/$FNAME" -fi - -if [ "x$1" = "xinstall" ]; then - [ "x$CI_CREDS" = "x" ] && { echo "CI_CREDS is not set: not downloading"; exit 1; } - # cleaning - rm -rf "$DESTDIR$CI_INSTALL_PREFIX"/* 2>/dev/null - rm -f "$DIR/$FNAME" 2>/dev/null - # downloading - curl -u "$CI_CREDS" -o "$DIR/$FNAME" "$CI_REPO_URL/$FNAME" - [ -f "$DIR/$FNAME" ] || { echo "failed to download $DIR/$FNAME"; exit 0; } - # installing - mkdir -p $DESTDIR - tar -C "$DESTDIR" -xvzf "$DIR/$FNAME" - exit 0 -fi
@@ -1,181 +0,0 @@
-#!/usr/bin/perl -## -## This script will help update manifest.yaml and Changelog.md before a release -## Any merge to master that changes the version line in manifest.yaml -## is considered as a new release. -## -## When ready to make a release, please run ./scripts/prepare-release -## and commit push the final result! -use File::Basename; -use Cwd 'abs_path'; - -# find its way to the root of git's repository -my $scriptsdirname = dirname(abs_path(__FILE__)); -chdir "$scriptsdirname/.."; -print "✓ Entering directory:".`pwd`; - -# ensures that the current branch is ahead of origin/main -my $diff= `git diff`; -chop $diff; -if ($diff =~ /./) { - die("ERROR: Please commit all the changes before calling the prepare-release script."); -} else { - print("✓ All changes are comitted.\n"); -} -system("git fetch origin"); -my $vcount = `git rev-list --left-right --count origin/main...HEAD`; -$vcount =~ /^([0-9]+)[ \t]*([0-9]+)$/; -if ($2>0) { - die("ERROR: the current HEAD is not ahead of origin/main\n. Please use git merge origin/main."); -} else { - print("✓ Current HEAD is up to date with origin/main.\n"); -} - -mkdir ".changes"; -my $currentbranch = `git rev-parse --abbrev-ref HEAD`; -chop $currentbranch; -$currentbranch =~ s/[^a-zA-Z._-]+/-/g; -my $changefile=".changes/$currentbranch.md"; -my $origmanifestfile=".changes/$currentbranch--manifest.yaml"; -my $origchangelogfile=".changes/$currentbranch--Changelog.md"; - -my $exit_code=system("wget -O $origmanifestfile https://raw.githubusercontent.com/tfhe/spqlios-fft/main/manifest.yaml"); -if ($exit_code!=0 or ! -f $origmanifestfile) { - die("ERROR: failed to download manifest.yaml"); -} -$exit_code=system("wget -O $origchangelogfile https://raw.githubusercontent.com/tfhe/spqlios-fft/main/Changelog.md"); -if ($exit_code!=0 or ! -f $origchangelogfile) { - die("ERROR: failed to download Changelog.md"); -} - -# read the current version (from origin/main manifest) -my $vmajor = 0; -my $vminor = 0; -my $vpatch = 0; -my $versionline = `grep '^version: ' $origmanifestfile | cut -d" " -f2`; -chop $versionline; -if (not $versionline =~ /^([0-9]+)\.([0-9]+)\.([0-9]+)$/) { - die("ERROR: invalid version in manifest file: $versionline\n"); -} else { - $vmajor = int($1); - $vminor = int($2); - $vpatch = int($3); -} -print "Version in manifest file: $vmajor.$vminor.$vpatch\n"; - -if (not -f $changefile) { - ## create a changes file - open F,">$changefile"; - print F "# Changefile for branch $currentbranch\n\n"; - print F "## Type of release (major,minor,patch)?\n\n"; - print F "releasetype: patch\n\n"; - print F "## What has changed (please edit)?\n\n"; - print F "- This has changed.\n"; - close F; -} - -system("editor $changefile"); - -# compute the new version -my $nvmajor; -my $nvminor; -my $nvpatch; -my $changelog; -my $recordchangelog=0; -open F,"$changefile"; -while ($line=<F>) { - chop $line; - if ($recordchangelog) { - ($line =~ /^$/) and next; - $changelog .= "$line\n"; - next; - } - if ($line =~ /^releasetype *: *patch *$/) { - $nvmajor=$vmajor; - $nvminor=$vminor; - $nvpatch=$vpatch+1; - } - if ($line =~ /^releasetype *: *minor *$/) { - $nvmajor=$vmajor; - $nvminor=$vminor+1; - $nvpatch=0; - } - if ($line =~ /^releasetype *: *major *$/) { - $nvmajor=$vmajor+1; - $nvminor=0; - $nvpatch=0; - } - if ($line =~ /^## What has changed/) { - $recordchangelog=1; - } -} -close F; -print "New version: $nvmajor.$nvminor.$nvpatch\n"; -print "Changes:\n$changelog"; - -# updating manifest.yaml -open F,"manifest.yaml"; -open G,">.changes/manifest.yaml"; -while ($line=<F>) { - if ($line =~ /^version *: */) { - print G "version: $nvmajor.$nvminor.$nvpatch\n"; - next; - } - print G $line; -} -close F; -close G; -# updating Changelog.md -open F,"$origchangelogfile"; -open G,">.changes/Changelog.md"; -print G <<EOF -# Changelog - -All notable changes to this project will be documented in this file. -this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -EOF -; -print G "## [$nvmajor.$nvminor.$nvpatch] - ".`date '+%Y-%m-%d'`."\n"; -print G "$changelog\n"; -my $skip_section=1; -while ($line=<F>) { - if ($line =~ /^## +\[([0-9]+)\.([0-9]+)\.([0-9]+)\] +/) { - if ($1>$nvmajor) { - die("ERROR: found larger version $1.$2.$3 in the Changelog.md\n"); - } elsif ($1<$nvmajor) { - $skip_section=0; - } elsif ($2>$nvminor) { - die("ERROR: found larger version $1.$2.$3 in the Changelog.md\n"); - } elsif ($2<$nvminor) { - $skip_section=0; - } elsif ($3>$nvpatch) { - die("ERROR: found larger version $1.$2.$3 in the Changelog.md\n"); - } elsif ($2<$nvpatch) { - $skip_section=0; - } else { - $skip_section=1; - } - } - ($skip_section) and next; - print G $line; -} -close F; -close G; - -print "-------------------------------------\n"; -print "THIS WILL BE UPDATED:\n"; -print "-------------------------------------\n"; -system("diff -u manifest.yaml .changes/manifest.yaml"); -system("diff -u Changelog.md .changes/Changelog.md"); -print "-------------------------------------\n"; -print "To proceed: press <enter> otherwise <CTRL+C>\n"; -my $bla; -$bla=<STDIN>; -system("cp -vf .changes/manifest.yaml manifest.yaml"); -system("cp -vf .changes/Changelog.md Changelog.md"); -system("git commit -a -m \"Update version and changelog.\""); -system("git push"); -print("✓ Changes have been committed and pushed!\n"); -print("✓ A new release will be created when this branch is merged to main.\n"); -
@@ -1,223 +0,0 @@
-enable_language(ASM) - -# C source files that are compiled for all targets (i.e. reference code) -set(SRCS_GENERIC - commons.c - commons_private.c - coeffs/coeffs_arithmetic.c - arithmetic/vec_znx.c - arithmetic/vec_znx_dft.c - arithmetic/vector_matrix_product.c - cplx/cplx_common.c - cplx/cplx_conversions.c - cplx/cplx_fft_asserts.c - cplx/cplx_fft_ref.c - cplx/cplx_fftvec_ref.c - cplx/cplx_ifft_ref.c - cplx/spqlios_cplx_fft.c - reim4/reim4_arithmetic_ref.c - reim4/reim4_fftvec_addmul_ref.c - reim4/reim4_fftvec_conv_ref.c - reim/reim_conversions.c - reim/reim_fft_ifft.c - reim/reim_fft_ref.c - reim/reim_fftvec_ref.c - reim/reim_ifft_ref.c - reim/reim_ifft_ref.c - reim/reim_to_tnx_ref.c - q120/q120_ntt.c - q120/q120_arithmetic_ref.c - q120/q120_arithmetic_simple.c - arithmetic/scalar_vector_product.c - arithmetic/vec_znx_big.c - arithmetic/znx_small.c - arithmetic/module_api.c - arithmetic/zn_vmp_int8_ref.c - arithmetic/zn_vmp_int16_ref.c - arithmetic/zn_vmp_int32_ref.c - arithmetic/zn_vmp_ref.c - arithmetic/zn_api.c - arithmetic/zn_conversions_ref.c - arithmetic/zn_approxdecomp_ref.c - arithmetic/vec_rnx_api.c - arithmetic/vec_rnx_conversions_ref.c - arithmetic/vec_rnx_svp_ref.c - reim/reim_execute.c - cplx/cplx_execute.c - reim4/reim4_execute.c - arithmetic/vec_rnx_arithmetic.c - arithmetic/vec_rnx_approxdecomp_ref.c - arithmetic/vec_rnx_vmp_ref.c -) -# C or assembly source files compiled only on x86 targets -set(SRCS_X86 - ) -# C or assembly source files compiled only on aarch64 targets -set(SRCS_AARCH64 - cplx/cplx_fallbacks_aarch64.c - reim/reim_fallbacks_aarch64.c - reim4/reim4_fallbacks_aarch64.c - q120/q120_fallbacks_aarch64.c - reim/reim_fft_neon.c -) - -# C or assembly source files compiled only on x86: avx, avx2, fma targets -set(SRCS_FMA_C - arithmetic/vector_matrix_product_avx.c - cplx/cplx_conversions_avx2_fma.c - cplx/cplx_fft_avx2_fma.c - cplx/cplx_fft_sse.c - cplx/cplx_fftvec_avx2_fma.c - cplx/cplx_ifft_avx2_fma.c - reim4/reim4_arithmetic_avx2.c - reim4/reim4_fftvec_conv_fma.c - reim4/reim4_fftvec_addmul_fma.c - reim/reim_conversions_avx.c - reim/reim_fft4_avx_fma.c - reim/reim_fft8_avx_fma.c - reim/reim_ifft4_avx_fma.c - reim/reim_ifft8_avx_fma.c - reim/reim_fft_avx2.c - reim/reim_ifft_avx2.c - reim/reim_to_tnx_avx.c - reim/reim_fftvec_fma.c -) -set(SRCS_FMA_ASM - cplx/cplx_fft16_avx_fma.s - cplx/cplx_ifft16_avx_fma.s - reim/reim_fft16_avx_fma.s - reim/reim_ifft16_avx_fma.s -) -set(SRCS_FMA_WIN32_ASM - cplx/cplx_fft16_avx_fma_win32.s - cplx/cplx_ifft16_avx_fma_win32.s - reim/reim_fft16_avx_fma_win32.s - reim/reim_ifft16_avx_fma_win32.s -) -set_source_files_properties(${SRCS_FMA_C} PROPERTIES COMPILE_OPTIONS "-mfma;-mavx;-mavx2") -set_source_files_properties(${SRCS_FMA_ASM} PROPERTIES COMPILE_OPTIONS "-mfma;-mavx;-mavx2") - -# C or assembly source files compiled only on x86: avx512f/vl/dq + fma targets -set(SRCS_AVX512 - cplx/cplx_fft_avx512.c - ) -set_source_files_properties(${SRCS_AVX512} PROPERTIES COMPILE_OPTIONS "-mfma;-mavx512f;-mavx512vl;-mavx512dq") - -# C or assembly source files compiled only on x86: avx2 + bmi targets -set(SRCS_AVX2 - arithmetic/vec_znx_avx.c - coeffs/coeffs_arithmetic_avx.c - arithmetic/vec_znx_dft_avx2.c - arithmetic/zn_vmp_int8_avx.c - arithmetic/zn_vmp_int16_avx.c - arithmetic/zn_vmp_int32_avx.c - q120/q120_arithmetic_avx2.c - q120/q120_ntt_avx2.c - arithmetic/vec_rnx_arithmetic_avx.c - arithmetic/vec_rnx_approxdecomp_avx.c - arithmetic/vec_rnx_vmp_avx.c - -) -set_source_files_properties(${SRCS_AVX2} PROPERTIES COMPILE_OPTIONS "-mbmi2;-mavx2") - -# C source files on float128 via libquadmath on x86 targets targets -set(SRCS_F128 - cplx_f128/cplx_fft_f128.c - cplx_f128/cplx_fft_f128.h - ) - -# H header files containing the public API (these headers are installed) -set(HEADERSPUBLIC - commons.h - arithmetic/vec_znx_arithmetic.h - arithmetic/vec_rnx_arithmetic.h - arithmetic/zn_arithmetic.h - cplx/cplx_fft.h - reim/reim_fft.h - q120/q120_common.h - q120/q120_arithmetic.h - q120/q120_ntt.h - ) - -# H header files containing the private API (these headers are used internally) -set(HEADERSPRIVATE - commons_private.h - cplx/cplx_fft_internal.h - cplx/cplx_fft_private.h - reim4/reim4_arithmetic.h - reim4/reim4_fftvec_internal.h - reim4/reim4_fftvec_private.h - reim4/reim4_fftvec_public.h - reim/reim_fft_internal.h - reim/reim_fft_private.h - q120/q120_arithmetic_private.h - q120/q120_ntt_private.h - arithmetic/vec_znx_arithmetic.h - arithmetic/vec_rnx_arithmetic_private.h - arithmetic/vec_rnx_arithmetic_plugin.h - arithmetic/zn_arithmetic_private.h - arithmetic/zn_arithmetic_plugin.h - coeffs/coeffs_arithmetic.h - reim/reim_fft_core_template.h -) - -set(SPQLIOSSOURCES - ${SRCS_GENERIC} - ${HEADERSPUBLIC} - ${HEADERSPRIVATE} - ) -if (${X86}) - set(SPQLIOSSOURCES ${SPQLIOSSOURCES} - ${SRCS_X86} - ${SRCS_FMA_C} - ${SRCS_FMA_ASM} - ${SRCS_AVX2} - ${SRCS_AVX512} - ) -elseif (${X86_WIN32}) - set(SPQLIOSSOURCES ${SPQLIOSSOURCES} - #${SRCS_X86} - ${SRCS_FMA_C} - ${SRCS_FMA_WIN32_ASM} - ${SRCS_AVX2} - ${SRCS_AVX512} - ) -elseif (${AARCH64}) - set(SPQLIOSSOURCES ${SPQLIOSSOURCES} - ${SRCS_AARCH64} - ) -endif () - - -set(SPQLIOSLIBDEP - m # libmath depencency for cosinus/sinus functions - ) - -if (ENABLE_SPQLIOS_F128) - find_library(quadmath REQUIRED NAMES quadmath) - set(SPQLIOSSOURCES ${SPQLIOSSOURCES} ${SRCS_F128}) - set(SPQLIOSLIBDEP ${SPQLIOSLIBDEP} quadmath) -endif (ENABLE_SPQLIOS_F128) - -add_library(libspqlios-static STATIC ${SPQLIOSSOURCES}) -add_library(libspqlios SHARED ${SPQLIOSSOURCES}) -set_property(TARGET libspqlios-static PROPERTY POSITION_INDEPENDENT_CODE ON) -set_property(TARGET libspqlios PROPERTY OUTPUT_NAME spqlios) -set_property(TARGET libspqlios-static PROPERTY OUTPUT_NAME spqlios) -set_property(TARGET libspqlios PROPERTY POSITION_INDEPENDENT_CODE ON) -set_property(TARGET libspqlios PROPERTY SOVERSION ${SPQLIOS_VERSION_MAJOR}) -set_property(TARGET libspqlios PROPERTY VERSION ${SPQLIOS_VERSION}) -if (NOT APPLE) -target_link_options(libspqlios-static PUBLIC -Wl,--no-undefined) -target_link_options(libspqlios PUBLIC -Wl,--no-undefined) -endif() -target_link_libraries(libspqlios ${SPQLIOSLIBDEP}) -target_link_libraries(libspqlios-static ${SPQLIOSLIBDEP}) -install(TARGETS libspqlios-static) -install(TARGETS libspqlios) - -# install the public headers only -foreach (file ${HEADERSPUBLIC}) - get_filename_component(dir ${file} DIRECTORY) - install(FILES ${file} DESTINATION include/spqlios/${dir}) -endforeach ()
@@ -1,172 +0,0 @@
-#include <string.h> - -#include "vec_znx_arithmetic_private.h" - -static void fill_generic_virtual_table(MODULE* module) { - // TODO add default ref handler here - module->func.vec_znx_zero = vec_znx_zero_ref; - module->func.vec_znx_copy = vec_znx_copy_ref; - module->func.vec_znx_negate = vec_znx_negate_ref; - module->func.vec_znx_add = vec_znx_add_ref; - module->func.vec_znx_sub = vec_znx_sub_ref; - module->func.vec_znx_rotate = vec_znx_rotate_ref; - module->func.vec_znx_mul_xp_minus_one = vec_znx_mul_xp_minus_one_ref; - module->func.vec_znx_automorphism = vec_znx_automorphism_ref; - module->func.vec_znx_normalize_base2k = vec_znx_normalize_base2k_ref; - module->func.vec_znx_normalize_base2k_tmp_bytes = vec_znx_normalize_base2k_tmp_bytes_ref; - if (CPU_SUPPORTS("avx2")) { - // TODO add avx handlers here - module->func.vec_znx_negate = vec_znx_negate_avx; - module->func.vec_znx_add = vec_znx_add_avx; - module->func.vec_znx_sub = vec_znx_sub_avx; - } -} - -static void fill_fft64_virtual_table(MODULE* module) { - // TODO add default ref handler here - // module->func.vec_znx_dft = ...; - module->func.vec_znx_big_normalize_base2k = fft64_vec_znx_big_normalize_base2k; - module->func.vec_znx_big_normalize_base2k_tmp_bytes = fft64_vec_znx_big_normalize_base2k_tmp_bytes; - module->func.vec_znx_big_range_normalize_base2k = fft64_vec_znx_big_range_normalize_base2k; - module->func.vec_znx_big_range_normalize_base2k_tmp_bytes = fft64_vec_znx_big_range_normalize_base2k_tmp_bytes; - module->func.vec_znx_dft = fft64_vec_znx_dft; - module->func.vec_znx_idft = fft64_vec_znx_idft; - module->func.vec_dft_add = fft64_vec_dft_add; - module->func.vec_dft_sub = fft64_vec_dft_sub; - module->func.vec_znx_idft_tmp_bytes = fft64_vec_znx_idft_tmp_bytes; - module->func.vec_znx_idft_tmp_a = fft64_vec_znx_idft_tmp_a; - module->func.vec_znx_big_add = fft64_vec_znx_big_add; - module->func.vec_znx_big_add_small = fft64_vec_znx_big_add_small; - module->func.vec_znx_big_add_small2 = fft64_vec_znx_big_add_small2; - module->func.vec_znx_big_sub = fft64_vec_znx_big_sub; - module->func.vec_znx_big_sub_small_a = fft64_vec_znx_big_sub_small_a; - module->func.vec_znx_big_sub_small_b = fft64_vec_znx_big_sub_small_b; - module->func.vec_znx_big_sub_small2 = fft64_vec_znx_big_sub_small2; - module->func.vec_znx_big_rotate = fft64_vec_znx_big_rotate; - module->func.vec_znx_big_automorphism = fft64_vec_znx_big_automorphism; - module->func.svp_prepare = fft64_svp_prepare_ref; - module->func.svp_apply_dft = fft64_svp_apply_dft_ref; - module->func.svp_apply_dft_to_dft = fft64_svp_apply_dft_to_dft_ref; - module->func.znx_small_single_product = fft64_znx_small_single_product; - module->func.znx_small_single_product_tmp_bytes = fft64_znx_small_single_product_tmp_bytes; - module->func.vmp_prepare_contiguous = fft64_vmp_prepare_contiguous_ref; - module->func.vmp_prepare_tmp_bytes = fft64_vmp_prepare_tmp_bytes; - module->func.vmp_apply_dft = fft64_vmp_apply_dft_ref; - module->func.vmp_apply_dft_add = fft64_vmp_apply_dft_add_ref; - module->func.vmp_apply_dft_tmp_bytes = fft64_vmp_apply_dft_tmp_bytes; - module->func.vmp_apply_dft_to_dft = fft64_vmp_apply_dft_to_dft_ref; - module->func.vmp_apply_dft_to_dft_add = fft64_vmp_apply_dft_to_dft_add_ref; - module->func.vmp_apply_dft_to_dft_tmp_bytes = fft64_vmp_apply_dft_to_dft_tmp_bytes; - module->func.bytes_of_vec_znx_dft = fft64_bytes_of_vec_znx_dft; - module->func.bytes_of_vec_znx_big = fft64_bytes_of_vec_znx_big; - module->func.bytes_of_svp_ppol = fft64_bytes_of_svp_ppol; - module->func.bytes_of_vmp_pmat = fft64_bytes_of_vmp_pmat; - if (CPU_SUPPORTS("avx2")) { - // TODO add avx handlers here - // TODO: enable when avx implementation is done - module->func.vmp_prepare_contiguous = fft64_vmp_prepare_contiguous_avx; - module->func.vmp_apply_dft = fft64_vmp_apply_dft_avx; - module->func.vmp_apply_dft_add = fft64_vmp_apply_dft_add_avx; - module->func.vmp_apply_dft_to_dft = fft64_vmp_apply_dft_to_dft_avx; - module->func.vmp_apply_dft_to_dft_add = fft64_vmp_apply_dft_to_dft_add_avx; - } -} - -static void fill_ntt120_virtual_table(MODULE* module) { - // TODO add default ref handler here - // module->func.vec_znx_dft = ...; - if (CPU_SUPPORTS("avx2")) { - // TODO add avx handlers here - module->func.vec_znx_dft = ntt120_vec_znx_dft_avx; - module->func.vec_znx_idft = ntt120_vec_znx_idft_avx; - module->func.vec_znx_idft_tmp_bytes = ntt120_vec_znx_idft_tmp_bytes_avx; - module->func.vec_znx_idft_tmp_a = ntt120_vec_znx_idft_tmp_a_avx; - } -} - -static void fill_virtual_table(MODULE* module) { - fill_generic_virtual_table(module); - switch (module->module_type) { - case FFT64: - fill_fft64_virtual_table(module); - break; - case NTT120: - fill_ntt120_virtual_table(module); - break; - default: - NOT_SUPPORTED(); // invalid type - } -} - -static void fill_fft64_precomp(MODULE* module) { - // fill any necessary precomp stuff - module->mod.fft64.p_conv = new_reim_from_znx64_precomp(module->m, 50); - module->mod.fft64.p_fft = new_reim_fft_precomp(module->m, 0); - module->mod.fft64.p_reim_to_znx = new_reim_to_znx64_precomp(module->m, module->m, 63); - module->mod.fft64.p_ifft = new_reim_ifft_precomp(module->m, 0); - module->mod.fft64.p_addmul = new_reim_fftvec_addmul_precomp(module->m); - module->mod.fft64.mul_fft = new_reim_fftvec_mul_precomp(module->m); - module->mod.fft64.add_fft = new_reim_fftvec_add_precomp(module->m); - module->mod.fft64.sub_fft = new_reim_fftvec_sub_precomp(module->m); -} -static void fill_ntt120_precomp(MODULE* module) { - // fill any necessary precomp stuff - if (CPU_SUPPORTS("avx2")) { - module->mod.q120.p_ntt = q120_new_ntt_bb_precomp(module->nn); - module->mod.q120.p_intt = q120_new_intt_bb_precomp(module->nn); - } -} - -static void fill_module_precomp(MODULE* module) { - switch (module->module_type) { - case FFT64: - fill_fft64_precomp(module); - break; - case NTT120: - fill_ntt120_precomp(module); - break; - default: - NOT_SUPPORTED(); // invalid type - } -} - -static void fill_module(MODULE* module, uint64_t nn, MODULE_TYPE mtype) { - // init to zero to ensure that any non-initialized field bug is detected - // by at least a "proper" segfault - memset(module, 0, sizeof(MODULE)); - module->module_type = mtype; - module->nn = nn; - module->m = nn >> 1; - fill_module_precomp(module); - fill_virtual_table(module); -} - -EXPORT MODULE* new_module_info(uint64_t N, MODULE_TYPE mtype) { - MODULE* m = (MODULE*)malloc(sizeof(MODULE)); - fill_module(m, N, mtype); - return m; -} - -EXPORT void delete_module_info(MODULE* mod) { - switch (mod->module_type) { - case FFT64: - free(mod->mod.fft64.p_conv); - free(mod->mod.fft64.p_fft); - free(mod->mod.fft64.p_ifft); - free(mod->mod.fft64.p_reim_to_znx); - free(mod->mod.fft64.mul_fft); - free(mod->mod.fft64.p_addmul); - break; - case NTT120: - if (CPU_SUPPORTS("avx2")) { - q120_del_ntt_bb_precomp(mod->mod.q120.p_ntt); - q120_del_intt_bb_precomp(mod->mod.q120.p_intt); - } - break; - default: - break; - } - free(mod); -} - -EXPORT uint64_t module_get_n(const MODULE* module) { return module->nn; }
@@ -1,102 +0,0 @@
-#include <string.h> - -#include "vec_znx_arithmetic_private.h" - -EXPORT uint64_t bytes_of_svp_ppol(const MODULE* module) { return module->func.bytes_of_svp_ppol(module); } - -EXPORT uint64_t fft64_bytes_of_svp_ppol(const MODULE* module) { return module->nn * sizeof(double); } - -EXPORT SVP_PPOL* new_svp_ppol(const MODULE* module) { return spqlios_alloc(bytes_of_svp_ppol(module)); } - -EXPORT void delete_svp_ppol(SVP_PPOL* ppol) { spqlios_free(ppol); } - -// public wrappers -EXPORT void svp_prepare(const MODULE* module, // N - SVP_PPOL* ppol, // output - const int64_t* pol // a -) { - module->func.svp_prepare(module, ppol, pol); -} - -/** @brief prepares a svp polynomial */ -EXPORT void fft64_svp_prepare_ref(const MODULE* module, // N - SVP_PPOL* ppol, // output - const int64_t* pol // a -) { - reim_from_znx64(module->mod.fft64.p_conv, ppol, pol); - reim_fft(module->mod.fft64.p_fft, (double*)ppol); -} - -EXPORT void svp_apply_dft(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, // output - const SVP_PPOL* ppol, // prepared pol - const int64_t* a, uint64_t a_size, uint64_t a_sl) { - module->func.svp_apply_dft(module, // N - res, - res_size, // output - ppol, // prepared pol - a, a_size, a_sl); -} - -EXPORT void svp_apply_dft_to_dft(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, - uint64_t res_cols, // output - const SVP_PPOL* ppol, // prepared pol - const VEC_ZNX_DFT* a, uint64_t a_size, uint64_t a_cols) { - module->func.svp_apply_dft_to_dft(module, // N - res, res_size, res_cols, // output - ppol, a, a_size, a_cols // prepared pol - ); -} - -// result = ppol * a -EXPORT void fft64_svp_apply_dft_ref(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, // output - const SVP_PPOL* ppol, // prepared pol - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->nn; - double* const dres = (double*)res; - double* const dppol = (double*)ppol; - - const uint64_t auto_end_idx = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < auto_end_idx; ++i) { - const int64_t* a_ptr = a + i * a_sl; - double* const res_ptr = dres + i * nn; - // copy the polynomial to res, apply fft in place, call fftvec_mul in place. - reim_from_znx64(module->mod.fft64.p_conv, res_ptr, a_ptr); - reim_fft(module->mod.fft64.p_fft, res_ptr); - reim_fftvec_mul(module->mod.fft64.mul_fft, res_ptr, res_ptr, dppol); - } - - // then extend with zeros - memset(dres + auto_end_idx * nn, 0, (res_size - auto_end_idx) * nn * sizeof(double)); -} - -// result = ppol * a -EXPORT void fft64_svp_apply_dft_to_dft_ref(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, - uint64_t res_cols, // output - const SVP_PPOL* ppol, // prepared pol - const VEC_ZNX_DFT* a, uint64_t a_size, - uint64_t a_cols // a -) { - const uint64_t nn = module->nn; - const uint64_t res_sl = nn * res_cols; - const uint64_t a_sl = nn * a_cols; - double* const dres = (double*)res; - double* const da = (double*)a; - double* const dppol = (double*)ppol; - - const uint64_t auto_end_idx = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < auto_end_idx; ++i) { - const double* a_ptr = da + i * a_sl; - double* const res_ptr = dres + i * res_sl; - reim_fftvec_mul(module->mod.fft64.mul_fft, res_ptr, a_ptr, dppol); - } - - // then extend with zeros - for (uint64_t i = auto_end_idx; i < res_size; i++) { - memset(dres + i * res_sl, 0, nn * sizeof(double)); - } -}
@@ -1,344 +0,0 @@
-#include <string.h> - -#include "vec_rnx_arithmetic_private.h" - -void fft64_init_rnx_module_precomp(MOD_RNX* module) { - // Add here initialization of items that are in the precomp - const uint64_t m = module->m; - module->precomp.fft64.p_fft = new_reim_fft_precomp(m, 0); - module->precomp.fft64.p_ifft = new_reim_ifft_precomp(m, 0); - module->precomp.fft64.p_fftvec_add = new_reim_fftvec_add_precomp(m); - module->precomp.fft64.p_fftvec_mul = new_reim_fftvec_mul_precomp(m); - module->precomp.fft64.p_fftvec_addmul = new_reim_fftvec_addmul_precomp(m); -} - -void fft64_finalize_rnx_module_precomp(MOD_RNX* module) { - // Add here deleters for items that are in the precomp - delete_reim_fft_precomp(module->precomp.fft64.p_fft); - delete_reim_ifft_precomp(module->precomp.fft64.p_ifft); - delete_reim_fftvec_add_precomp(module->precomp.fft64.p_fftvec_add); - delete_reim_fftvec_mul_precomp(module->precomp.fft64.p_fftvec_mul); - delete_reim_fftvec_addmul_precomp(module->precomp.fft64.p_fftvec_addmul); -} - -void fft64_init_rnx_module_vtable(MOD_RNX* module) { - // Add function pointers here - module->vtable.vec_rnx_add = vec_rnx_add_ref; - module->vtable.vec_rnx_zero = vec_rnx_zero_ref; - module->vtable.vec_rnx_copy = vec_rnx_copy_ref; - module->vtable.vec_rnx_negate = vec_rnx_negate_ref; - module->vtable.vec_rnx_sub = vec_rnx_sub_ref; - module->vtable.vec_rnx_rotate = vec_rnx_rotate_ref; - module->vtable.vec_rnx_automorphism = vec_rnx_automorphism_ref; - module->vtable.vec_rnx_mul_xp_minus_one = vec_rnx_mul_xp_minus_one_ref; - module->vtable.rnx_vmp_apply_dft_to_dft_tmp_bytes = fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref; - module->vtable.rnx_vmp_apply_dft_to_dft = fft64_rnx_vmp_apply_dft_to_dft_ref; - module->vtable.rnx_vmp_apply_tmp_a_tmp_bytes = fft64_rnx_vmp_apply_tmp_a_tmp_bytes_ref; - module->vtable.rnx_vmp_apply_tmp_a = fft64_rnx_vmp_apply_tmp_a_ref; - module->vtable.rnx_vmp_prepare_tmp_bytes = fft64_rnx_vmp_prepare_tmp_bytes_ref; - module->vtable.rnx_vmp_prepare_contiguous = fft64_rnx_vmp_prepare_contiguous_ref; - module->vtable.rnx_vmp_prepare_dblptr = fft64_rnx_vmp_prepare_dblptr_ref; - module->vtable.rnx_vmp_prepare_row = fft64_rnx_vmp_prepare_row_ref; - module->vtable.bytes_of_rnx_vmp_pmat = fft64_bytes_of_rnx_vmp_pmat; - module->vtable.rnx_approxdecomp_from_tnxdbl = rnx_approxdecomp_from_tnxdbl_ref; - module->vtable.vec_rnx_to_znx32 = vec_rnx_to_znx32_ref; - module->vtable.vec_rnx_from_znx32 = vec_rnx_from_znx32_ref; - module->vtable.vec_rnx_to_tnx32 = vec_rnx_to_tnx32_ref; - module->vtable.vec_rnx_from_tnx32 = vec_rnx_from_tnx32_ref; - module->vtable.vec_rnx_to_tnxdbl = vec_rnx_to_tnxdbl_ref; - module->vtable.bytes_of_rnx_svp_ppol = fft64_bytes_of_rnx_svp_ppol; - module->vtable.rnx_svp_prepare = fft64_rnx_svp_prepare_ref; - module->vtable.rnx_svp_apply = fft64_rnx_svp_apply_ref; - - // Add optimized function pointers here - if (CPU_SUPPORTS("avx")) { - module->vtable.vec_rnx_add = vec_rnx_add_avx; - module->vtable.vec_rnx_sub = vec_rnx_sub_avx; - module->vtable.vec_rnx_negate = vec_rnx_negate_avx; - module->vtable.rnx_vmp_apply_dft_to_dft_tmp_bytes = fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_avx; - module->vtable.rnx_vmp_apply_dft_to_dft = fft64_rnx_vmp_apply_dft_to_dft_avx; - module->vtable.rnx_vmp_apply_tmp_a_tmp_bytes = fft64_rnx_vmp_apply_tmp_a_tmp_bytes_avx; - module->vtable.rnx_vmp_apply_tmp_a = fft64_rnx_vmp_apply_tmp_a_avx; - module->vtable.rnx_vmp_prepare_tmp_bytes = fft64_rnx_vmp_prepare_tmp_bytes_avx; - module->vtable.rnx_vmp_prepare_contiguous = fft64_rnx_vmp_prepare_contiguous_avx; - module->vtable.rnx_vmp_prepare_dblptr = fft64_rnx_vmp_prepare_dblptr_avx; - module->vtable.rnx_vmp_prepare_row = fft64_rnx_vmp_prepare_row_avx; - module->vtable.rnx_approxdecomp_from_tnxdbl = rnx_approxdecomp_from_tnxdbl_avx; - } -} - -void init_rnx_module_info(MOD_RNX* module, // - uint64_t n, RNX_MODULE_TYPE mtype) { - memset(module, 0, sizeof(MOD_RNX)); - module->n = n; - module->m = n >> 1; - module->mtype = mtype; - switch (mtype) { - case FFT64: - fft64_init_rnx_module_precomp(module); - fft64_init_rnx_module_vtable(module); - break; - default: - NOT_SUPPORTED(); // unknown mtype - } -} - -void finalize_rnx_module_info(MOD_RNX* module) { - if (module->custom) module->custom_deleter(module->custom); - switch (module->mtype) { - case FFT64: - fft64_finalize_rnx_module_precomp(module); - // fft64_finalize_rnx_module_vtable(module); // nothing to finalize - break; - default: - NOT_SUPPORTED(); // unknown mtype - } -} - -EXPORT MOD_RNX* new_rnx_module_info(uint64_t nn, RNX_MODULE_TYPE mtype) { - MOD_RNX* res = (MOD_RNX*)malloc(sizeof(MOD_RNX)); - init_rnx_module_info(res, nn, mtype); - return res; -} - -EXPORT void delete_rnx_module_info(MOD_RNX* module_info) { - finalize_rnx_module_info(module_info); - free(module_info); -} - -EXPORT uint64_t rnx_module_get_n(const MOD_RNX* module) { return module->n; } - -/** @brief allocates a prepared matrix (release with delete_rnx_vmp_pmat) */ -EXPORT RNX_VMP_PMAT* new_rnx_vmp_pmat(const MOD_RNX* module, // N - uint64_t nrows, uint64_t ncols) { // dimensions - return (RNX_VMP_PMAT*)spqlios_alloc(bytes_of_rnx_vmp_pmat(module, nrows, ncols)); -} -EXPORT void delete_rnx_vmp_pmat(RNX_VMP_PMAT* ptr) { spqlios_free(ptr); } - -//////////////// wrappers ////////////////// - -/** @brief sets res = a + b */ -EXPORT void vec_rnx_add( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -) { - module->vtable.vec_rnx_add(module, res, res_size, res_sl, a, a_size, a_sl, b, b_size, b_sl); -} - -/** @brief sets res = 0 */ -EXPORT void vec_rnx_zero( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl // res -) { - module->vtable.vec_rnx_zero(module, res, res_size, res_sl); -} - -/** @brief sets res = a */ -EXPORT void vec_rnx_copy( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_copy(module, res, res_size, res_sl, a, a_size, a_sl); -} - -/** @brief sets res = -a */ -EXPORT void vec_rnx_negate( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_negate(module, res, res_size, res_sl, a, a_size, a_sl); -} - -/** @brief sets res = a - b */ -EXPORT void vec_rnx_sub( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -) { - module->vtable.vec_rnx_sub(module, res, res_size, res_sl, a, a_size, a_sl, b, b_size, b_sl); -} - -/** @brief sets res = a . X^p */ -EXPORT void vec_rnx_rotate( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_rotate(module, p, res, res_size, res_sl, a, a_size, a_sl); -} - -/** @brief sets res = a(X^p) */ -EXPORT void vec_rnx_automorphism( // - const MOD_RNX* module, // N - int64_t p, // X -> X^p - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_automorphism(module, p, res, res_size, res_sl, a, a_size, a_sl); -} - -EXPORT void vec_rnx_mul_xp_minus_one( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_mul_xp_minus_one(module, p, res, res_size, res_sl, a, a_size, a_sl); -} -/** @brief number of bytes in a RNX_VMP_PMAT (for manual allocation) */ -EXPORT uint64_t bytes_of_rnx_vmp_pmat(const MOD_RNX* module, // N - uint64_t nrows, uint64_t ncols) { // dimensions - return module->vtable.bytes_of_rnx_vmp_pmat(module, nrows, ncols); -} - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void rnx_vmp_prepare_contiguous( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* a, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - module->vtable.rnx_vmp_prepare_contiguous(module, pmat, a, nrows, ncols, tmp_space); -} - -/** @brief prepares a vmp matrix (mat[row]+col*N points to the item) */ -EXPORT void rnx_vmp_prepare_dblptr( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double** a, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - module->vtable.rnx_vmp_prepare_dblptr(module, pmat, a, nrows, ncols, tmp_space); -} - -/** @brief prepares the ith-row of a vmp matrix with nrows and ncols */ -EXPORT void rnx_vmp_prepare_row( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* a, uint64_t row_i, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - module->vtable.rnx_vmp_prepare_row(module, pmat, a, row_i, nrows, ncols, tmp_space); -} - -/** @brief number of scratch bytes necessary to prepare a matrix */ -EXPORT uint64_t rnx_vmp_prepare_tmp_bytes(const MOD_RNX* module) { - return module->vtable.rnx_vmp_prepare_tmp_bytes(module); -} - -/** @brief applies a vmp product res = a x pmat */ -EXPORT void rnx_vmp_apply_tmp_a( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - double* tmpa, uint64_t a_size, uint64_t a_sl, // a (will be overwritten) - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -) { - module->vtable.rnx_vmp_apply_tmp_a(module, res, res_size, res_sl, tmpa, a_size, a_sl, pmat, nrows, ncols, tmp_space); -} - -EXPORT uint64_t rnx_vmp_apply_tmp_a_tmp_bytes( // - const MOD_RNX* module, // N - uint64_t res_size, // res size - uint64_t a_size, // a size - uint64_t nrows, uint64_t ncols // prep matrix dims -) { - return module->vtable.rnx_vmp_apply_tmp_a_tmp_bytes(module, res_size, a_size, nrows, ncols); -} - -/** @brief minimal size of the tmp_space */ -EXPORT void rnx_vmp_apply_dft_to_dft( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl, // a - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - module->vtable.rnx_vmp_apply_dft_to_dft(module, res, res_size, res_sl, a_dft, a_size, a_sl, pmat, nrows, ncols, - tmp_space); -} - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t rnx_vmp_apply_dft_to_dft_tmp_bytes( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - return module->vtable.rnx_vmp_apply_dft_to_dft_tmp_bytes(module, res_size, a_size, nrows, ncols); -} - -EXPORT uint64_t bytes_of_rnx_svp_ppol(const MOD_RNX* module) { return module->vtable.bytes_of_rnx_svp_ppol(module); } - -EXPORT void rnx_svp_prepare(const MOD_RNX* module, // N - RNX_SVP_PPOL* ppol, // output - const double* pol // a -) { - module->vtable.rnx_svp_prepare(module, ppol, pol); -} - -EXPORT void rnx_svp_apply( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // output - const RNX_SVP_PPOL* ppol, // prepared pol - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.rnx_svp_apply(module, // N - res, res_size, res_sl, // output - ppol, // prepared pol - a, a_size, a_sl); -} - -EXPORT void rnx_approxdecomp_from_tnxdbl( // - const MOD_RNX* module, // N - const TNXDBL_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a) { // a - module->vtable.rnx_approxdecomp_from_tnxdbl(module, gadget, res, res_size, res_sl, a); -} - -EXPORT void vec_rnx_to_znx32( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_to_znx32(module, res, res_size, res_sl, a, a_size, a_sl); -} - -EXPORT void vec_rnx_from_znx32( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_from_znx32(module, res, res_size, res_sl, a, a_size, a_sl); -} - -EXPORT void vec_rnx_to_tnx32( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_to_tnx32(module, res, res_size, res_sl, a, a_size, a_sl); -} - -EXPORT void vec_rnx_from_tnx32( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_from_tnx32(module, res, res_size, res_sl, a, a_size, a_sl); -} - -EXPORT void vec_rnx_to_tnxdbl( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - module->vtable.vec_rnx_to_tnxdbl(module, res, res_size, res_sl, a, a_size, a_sl); -}
@@ -1,59 +0,0 @@
-#include <memory.h> - -#include "immintrin.h" -#include "vec_rnx_arithmetic_private.h" - -/** @brief sets res = gadget_decompose(a) */ -EXPORT void rnx_approxdecomp_from_tnxdbl_avx( // - const MOD_RNX* module, // N - const TNXDBL_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a // a -) { - const uint64_t nn = module->n; - if (nn < 4) return rnx_approxdecomp_from_tnxdbl_ref(module, gadget, res, res_size, res_sl, a); - const uint64_t ell = gadget->ell; - const __m256i k = _mm256_set1_epi64x(gadget->k); - const __m256d add_cst = _mm256_set1_pd(gadget->add_cst); - const __m256i and_mask = _mm256_set1_epi64x(gadget->and_mask); - const __m256i or_mask = _mm256_set1_epi64x(gadget->or_mask); - const __m256d sub_cst = _mm256_set1_pd(gadget->sub_cst); - const uint64_t msize = res_size <= ell ? res_size : ell; - // gadget decompose column by column - if (msize == ell) { - // this is the main scenario when msize == ell - double* const last_r = res + (msize - 1) * res_sl; - for (uint64_t j = 0; j < nn; j += 4) { - double* rr = last_r + j; - const double* aa = a + j; - __m256d t_dbl = _mm256_add_pd(_mm256_loadu_pd(aa), add_cst); - __m256i t_int = _mm256_castpd_si256(t_dbl); - do { - __m256i u_int = _mm256_or_si256(_mm256_and_si256(t_int, and_mask), or_mask); - _mm256_storeu_pd(rr, _mm256_sub_pd(_mm256_castsi256_pd(u_int), sub_cst)); - t_int = _mm256_srlv_epi64(t_int, k); - rr -= res_sl; - } while (rr >= res); - } - } else if (msize > 0) { - // otherwise, if msize < ell: there is one additional rshift - const __m256i first_rsh = _mm256_set1_epi64x((ell - msize) * gadget->k); - double* const last_r = res + (msize - 1) * res_sl; - for (uint64_t j = 0; j < nn; j += 4) { - double* rr = last_r + j; - const double* aa = a + j; - __m256d t_dbl = _mm256_add_pd(_mm256_loadu_pd(aa), add_cst); - __m256i t_int = _mm256_srlv_epi64(_mm256_castpd_si256(t_dbl), first_rsh); - do { - __m256i u_int = _mm256_or_si256(_mm256_and_si256(t_int, and_mask), or_mask); - _mm256_storeu_pd(rr, _mm256_sub_pd(_mm256_castsi256_pd(u_int), sub_cst)); - t_int = _mm256_srlv_epi64(t_int, k); - rr -= res_sl; - } while (rr >= res); - } - } - // zero-out the last slices (if any) - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -}
@@ -1,75 +0,0 @@
-#include <memory.h> - -#include "vec_rnx_arithmetic_private.h" - -typedef union di { - double dv; - uint64_t uv; -} di_t; - -/** @brief new gadget: delete with delete_tnxdbl_approxdecomp_gadget */ -EXPORT TNXDBL_APPROXDECOMP_GADGET* new_tnxdbl_approxdecomp_gadget( // - const MOD_RNX* module, // N - uint64_t k, uint64_t ell // base 2^K and size -) { - if (k * ell > 50) return spqlios_error("gadget requires a too large fp precision"); - TNXDBL_APPROXDECOMP_GADGET* res = spqlios_alloc(sizeof(TNXDBL_APPROXDECOMP_GADGET)); - res->k = k; - res->ell = ell; - // double add_cst; // double(3.2^(51-ell.K) + 1/2.(sum 2^(-iK)) for i=[0,ell[) - union di add_cst; - add_cst.dv = UINT64_C(3) << (51 - ell * k); - for (uint64_t i = 0; i < ell; ++i) { - add_cst.uv |= UINT64_C(1) << ((i + 1) * k - 1); - } - res->add_cst = add_cst.dv; - // uint64_t and_mask; // uint64(2^(K)-1) - res->and_mask = (UINT64_C(1) << k) - 1; - // uint64_t or_mask; // double(2^52) - union di or_mask; - or_mask.dv = (UINT64_C(1) << 52); - res->or_mask = or_mask.uv; - // double sub_cst; // double(2^52 + 2^(K-1)) - res->sub_cst = ((UINT64_C(1) << 52) + (UINT64_C(1) << (k - 1))); - return res; -} - -EXPORT void delete_tnxdbl_approxdecomp_gadget(TNXDBL_APPROXDECOMP_GADGET* gadget) { spqlios_free(gadget); } - -/** @brief sets res = gadget_decompose(a) */ -EXPORT void rnx_approxdecomp_from_tnxdbl_ref( // - const MOD_RNX* module, // N - const TNXDBL_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a // a -) { - const uint64_t nn = module->n; - const uint64_t k = gadget->k; - const uint64_t ell = gadget->ell; - const double add_cst = gadget->add_cst; - const uint64_t and_mask = gadget->and_mask; - const uint64_t or_mask = gadget->or_mask; - const double sub_cst = gadget->sub_cst; - const uint64_t msize = res_size <= ell ? res_size : ell; - const uint64_t first_rsh = (ell - msize) * k; - // gadget decompose column by column - if (msize > 0) { - double* const last_r = res + (msize - 1) * res_sl; - for (uint64_t j = 0; j < nn; ++j) { - double* rr = last_r + j; - di_t t = {.dv = a[j] + add_cst}; - if (msize < ell) t.uv >>= first_rsh; - do { - di_t u; - u.uv = (t.uv & and_mask) | or_mask; - *rr = u.dv - sub_cst; - t.uv >>= k; - rr -= res_sl; - } while (rr >= res); - } - } - // zero-out the last slices (if any) - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -}
@@ -1,223 +0,0 @@
-#include <string.h> - -#include "../coeffs/coeffs_arithmetic.h" -#include "vec_rnx_arithmetic_private.h" - -void rnx_add_ref(uint64_t nn, double* res, const double* a, const double* b) { - for (uint64_t i = 0; i < nn; ++i) { - res[i] = a[i] + b[i]; - } -} - -void rnx_sub_ref(uint64_t nn, double* res, const double* a, const double* b) { - for (uint64_t i = 0; i < nn; ++i) { - res[i] = a[i] - b[i]; - } -} - -void rnx_negate_ref(uint64_t nn, double* res, const double* a) { - for (uint64_t i = 0; i < nn; ++i) { - res[i] = -a[i]; - } -} - -/** @brief sets res = a + b */ -EXPORT void vec_rnx_add_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->n; - if (a_size < b_size) { - const uint64_t msize = res_size < a_size ? res_size : a_size; - const uint64_t nsize = res_size < b_size ? res_size : b_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_add_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - memcpy(res + i * res_sl, b + i * b_sl, nn * sizeof(double)); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } else { - const uint64_t msize = res_size < b_size ? res_size : b_size; - const uint64_t nsize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_add_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - memcpy(res + i * res_sl, a + i * a_sl, nn * sizeof(double)); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } -} - -/** @brief sets res = 0 */ -EXPORT void vec_rnx_zero_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl // res -) { - const uint64_t nn = module->n; - for (uint64_t i = 0; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief sets res = a */ -EXPORT void vec_rnx_copy_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - // rotate up to the smallest dimension - for (uint64_t i = 0; i < rot_end_idx; ++i) { - double* res_ptr = res + i * res_sl; - const double* a_ptr = a + i * a_sl; - memcpy(res_ptr, a_ptr, nn * sizeof(double)); - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief sets res = -a */ -EXPORT void vec_rnx_negate_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - // rotate up to the smallest dimension - for (uint64_t i = 0; i < rot_end_idx; ++i) { - double* res_ptr = res + i * res_sl; - const double* a_ptr = a + i * a_sl; - rnx_negate_ref(nn, res_ptr, a_ptr); - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief sets res = a - b */ -EXPORT void vec_rnx_sub_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->n; - if (a_size < b_size) { - const uint64_t msize = res_size < a_size ? res_size : a_size; - const uint64_t nsize = res_size < b_size ? res_size : b_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_sub_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - rnx_negate_ref(nn, res + i * res_sl, b + i * b_sl); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } else { - const uint64_t msize = res_size < b_size ? res_size : b_size; - const uint64_t nsize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_sub_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - memcpy(res + i * res_sl, a + i * a_sl, nn * sizeof(double)); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } -} - -/** @brief sets res = a . X^p */ -EXPORT void vec_rnx_rotate_ref( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - // rotate up to the smallest dimension - for (uint64_t i = 0; i < rot_end_idx; ++i) { - double* res_ptr = res + i * res_sl; - const double* a_ptr = a + i * a_sl; - if (res_ptr == a_ptr) { - rnx_rotate_inplace_f64(nn, p, res_ptr); - } else { - rnx_rotate_f64(nn, p, res_ptr, a_ptr); - } - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief sets res = a(X^p) */ -EXPORT void vec_rnx_automorphism_ref( // - const MOD_RNX* module, // N - int64_t p, // X -> X^p - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - // rotate up to the smallest dimension - for (uint64_t i = 0; i < rot_end_idx; ++i) { - double* res_ptr = res + i * res_sl; - const double* a_ptr = a + i * a_sl; - if (res_ptr == a_ptr) { - rnx_automorphism_inplace_f64(nn, p, res_ptr); - } else { - rnx_automorphism_f64(nn, p, res_ptr, a_ptr); - } - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief sets res = a . (X^p - 1) */ -EXPORT void vec_rnx_mul_xp_minus_one_ref( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - // rotate up to the smallest dimension - for (uint64_t i = 0; i < rot_end_idx; ++i) { - double* res_ptr = res + i * res_sl; - const double* a_ptr = a + i * a_sl; - if (res_ptr == a_ptr) { - rnx_mul_xp_minus_one_inplace_f64(nn, p, res_ptr); - } else { - rnx_mul_xp_minus_one_f64(nn, p, res_ptr, a_ptr); - } - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -}
@@ -1,356 +0,0 @@
-#ifndef SPQLIOS_VEC_RNX_ARITHMETIC_H -#define SPQLIOS_VEC_RNX_ARITHMETIC_H - -#include <stdint.h> - -#include "../commons.h" - -/** - * We support the following module families: - * - FFT64: - * the overall precision should fit at all times over 52 bits. - */ -typedef enum rnx_module_type_t { FFT64 } RNX_MODULE_TYPE; - -/** @brief opaque structure that describes the modules (RnX,ZnX,TnX) and the hardware */ -typedef struct rnx_module_info_t MOD_RNX; - -/** - * @brief obtain a module info for ring dimension N - * the module-info knows about: - * - the dimension N (or the complex dimension m=N/2) - * - any moduleuted fft or ntt items - * - the hardware (avx, arm64, x86, ...) - */ -EXPORT MOD_RNX* new_rnx_module_info(uint64_t N, RNX_MODULE_TYPE mode); -EXPORT void delete_rnx_module_info(MOD_RNX* module_info); -EXPORT uint64_t rnx_module_get_n(const MOD_RNX* module); - -// basic arithmetic - -/** @brief sets res = 0 */ -EXPORT void vec_rnx_zero( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl // res -); - -/** @brief sets res = a */ -EXPORT void vec_rnx_copy( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = -a */ -EXPORT void vec_rnx_negate( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a + b */ -EXPORT void vec_rnx_add( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a - b */ -EXPORT void vec_rnx_sub( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a . X^p */ -EXPORT void vec_rnx_rotate( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a . (X^p - 1) */ -EXPORT void vec_rnx_mul_xp_minus_one( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a(X^p) */ -EXPORT void vec_rnx_automorphism( // - const MOD_RNX* module, // N - int64_t p, // X -> X^p - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/////////////////////////////////////////////////////////////////// -// conversions // -/////////////////////////////////////////////////////////////////// - -EXPORT void vec_rnx_to_znx32( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_from_znx32( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_to_tnx32( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_from_tnx32( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_to_tnx32x2( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_from_tnx32x2( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_to_tnxdbl( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/////////////////////////////////////////////////////////////////// -// isolated products (n.log(n), but not particularly optimized // -/////////////////////////////////////////////////////////////////// - -/** @brief res = a * b : small polynomial product */ -EXPORT void rnx_small_single_product( // - const MOD_RNX* module, // N - double* res, // output - const double* a, // a - const double* b, // b - uint8_t* tmp); // scratch space - -EXPORT uint64_t rnx_small_single_product_tmp_bytes(const MOD_RNX* module); - -/** @brief res = a * b centermod 1: small polynomial product */ -EXPORT void tnxdbl_small_single_product( // - const MOD_RNX* module, // N - double* torus_res, // output - const double* int_a, // a - const double* torus_b, // b - uint8_t* tmp); // scratch space - -EXPORT uint64_t tnxdbl_small_single_product_tmp_bytes(const MOD_RNX* module); - -/** @brief res = a * b: small polynomial product */ -EXPORT void znx32_small_single_product( // - const MOD_RNX* module, // N - int32_t* int_res, // output - const int32_t* int_a, // a - const int32_t* int_b, // b - uint8_t* tmp); // scratch space - -EXPORT uint64_t znx32_small_single_product_tmp_bytes(const MOD_RNX* module); - -/** @brief res = a * b centermod 1: small polynomial product */ -EXPORT void tnx32_small_single_product( // - const MOD_RNX* module, // N - int32_t* torus_res, // output - const int32_t* int_a, // a - const int32_t* torus_b, // b - uint8_t* tmp); // scratch space - -EXPORT uint64_t tnx32_small_single_product_tmp_bytes(const MOD_RNX* module); - -/////////////////////////////////////////////////////////////////// -// prepared gadget decompositions (optimized) // -/////////////////////////////////////////////////////////////////// - -// decompose from tnx32 - -typedef struct tnx32_approxdecomp_gadget_t TNX32_APPROXDECOMP_GADGET; - -/** @brief new gadget: delete with delete_tnx32_approxdecomp_gadget */ -EXPORT TNX32_APPROXDECOMP_GADGET* new_tnx32_approxdecomp_gadget( // - const MOD_RNX* module, // N - uint64_t k, uint64_t ell // base 2^K and size -); -EXPORT void delete_tnx32_approxdecomp_gadget(const MOD_RNX* module); - -/** @brief sets res = gadget_decompose(a) */ -EXPORT void rnx_approxdecomp_from_tnx32( // - const MOD_RNX* module, // N - const TNX32_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a // a -); - -// decompose from tnx32x2 - -typedef struct tnx32x2_approxdecomp_gadget_t TNX32X2_APPROXDECOMP_GADGET; - -/** @brief new gadget: delete with delete_tnx32x2_approxdecomp_gadget */ -EXPORT TNX32X2_APPROXDECOMP_GADGET* new_tnx32x2_approxdecomp_gadget(const MOD_RNX* module, uint64_t ka, uint64_t ella, - uint64_t kb, uint64_t ellb); -EXPORT void delete_tnx32x2_approxdecomp_gadget(const MOD_RNX* module); - -/** @brief sets res = gadget_decompose(a) */ -EXPORT void rnx_approxdecomp_from_tnx32x2( // - const MOD_RNX* module, // N - const TNX32X2_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a // a -); - -// decompose from tnxdbl - -typedef struct tnxdbl_approxdecomp_gadget_t TNXDBL_APPROXDECOMP_GADGET; - -/** @brief new gadget: delete with delete_tnxdbl_approxdecomp_gadget */ -EXPORT TNXDBL_APPROXDECOMP_GADGET* new_tnxdbl_approxdecomp_gadget( // - const MOD_RNX* module, // N - uint64_t k, uint64_t ell // base 2^K and size -); -EXPORT void delete_tnxdbl_approxdecomp_gadget(TNXDBL_APPROXDECOMP_GADGET* gadget); - -/** @brief sets res = gadget_decompose(a) */ -EXPORT void rnx_approxdecomp_from_tnxdbl( // - const MOD_RNX* module, // N - const TNXDBL_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a); // a - -/////////////////////////////////////////////////////////////////// -// prepared scalar-vector product (optimized) // -/////////////////////////////////////////////////////////////////// - -/** @brief opaque type that represents a polynomial of RnX prepared for a scalar-vector product */ -typedef struct rnx_svp_ppol_t RNX_SVP_PPOL; - -/** @brief number of bytes in a RNX_VMP_PMAT (for manual allocation) */ -EXPORT uint64_t bytes_of_rnx_svp_ppol(const MOD_RNX* module); // N - -/** @brief allocates a prepared vector (release with delete_rnx_svp_ppol) */ -EXPORT RNX_SVP_PPOL* new_rnx_svp_ppol(const MOD_RNX* module); // N - -/** @brief frees memory for a prepared vector */ -EXPORT void delete_rnx_svp_ppol(RNX_SVP_PPOL* res); - -/** @brief prepares a svp polynomial */ -EXPORT void rnx_svp_prepare(const MOD_RNX* module, // N - RNX_SVP_PPOL* ppol, // output - const double* pol // a -); - -/** @brief apply a svp product, result = ppol * a, presented in DFT space */ -EXPORT void rnx_svp_apply( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // output - const RNX_SVP_PPOL* ppol, // prepared pol - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/////////////////////////////////////////////////////////////////// -// prepared vector-matrix product (optimized) // -/////////////////////////////////////////////////////////////////// - -typedef struct rnx_vmp_pmat_t RNX_VMP_PMAT; - -/** @brief number of bytes in a RNX_VMP_PMAT (for manual allocation) */ -EXPORT uint64_t bytes_of_rnx_vmp_pmat(const MOD_RNX* module, // N - uint64_t nrows, uint64_t ncols); // dimensions - -/** @brief allocates a prepared matrix (release with delete_rnx_vmp_pmat) */ -EXPORT RNX_VMP_PMAT* new_rnx_vmp_pmat(const MOD_RNX* module, // N - uint64_t nrows, uint64_t ncols); // dimensions -EXPORT void delete_rnx_vmp_pmat(RNX_VMP_PMAT* ptr); - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void rnx_vmp_prepare_contiguous( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* a, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); - -/** @brief prepares a vmp matrix (mat[row]+col*N points to the item) */ -EXPORT void rnx_vmp_prepare_dblptr( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double** a, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); - -/** @brief prepares the ith-row of a vmp matrix with nrows and ncols */ -EXPORT void rnx_vmp_prepare_row( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* a, uint64_t row_i, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); - -/** @brief number of scratch bytes necessary to prepare a matrix */ -EXPORT uint64_t rnx_vmp_prepare_tmp_bytes(const MOD_RNX* module); - -/** @brief applies a vmp product res = a x pmat */ -EXPORT void rnx_vmp_apply_tmp_a( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - double* tmpa, uint64_t a_size, uint64_t a_sl, // a (will be overwritten) - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -); - -EXPORT uint64_t rnx_vmp_apply_tmp_a_tmp_bytes( // - const MOD_RNX* module, // N - uint64_t res_size, // res size - uint64_t a_size, // a size - uint64_t nrows, uint64_t ncols // prep matrix dims -); - -/** @brief minimal size of the tmp_space */ -EXPORT void rnx_vmp_apply_dft_to_dft( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl, // a - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t rnx_vmp_apply_dft_to_dft_tmp_bytes( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); - -/** @brief sets res = DFT(a) */ -EXPORT void vec_rnx_dft(const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = iDFT(a_dft) -- idft is not normalized */ -EXPORT void vec_rnx_idft(const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl // a -); - -#endif // SPQLIOS_VEC_RNX_ARITHMETIC_H
@@ -1,189 +0,0 @@
-#include <immintrin.h> -#include <string.h> - -#include "vec_rnx_arithmetic_private.h" - -void rnx_add_avx(uint64_t nn, double* res, const double* a, const double* b) { - if (nn < 8) { - if (nn == 4) { - _mm256_storeu_pd(res, _mm256_add_pd(_mm256_loadu_pd(a), _mm256_loadu_pd(b))); - } else if (nn == 2) { - _mm_storeu_pd(res, _mm_add_pd(_mm_loadu_pd(a), _mm_loadu_pd(b))); - } else if (nn == 1) { - *res = *a + *b; - } else { - NOT_SUPPORTED(); // not a power of 2 - } - return; - } - // general case: nn >= 8 - __m256d x0, x1, x2, x3, x4, x5; - const double* aa = a; - const double* bb = b; - double* rr = res; - double* const rrend = res + nn; - do { - x0 = _mm256_loadu_pd(aa); - x1 = _mm256_loadu_pd(aa + 4); - x2 = _mm256_loadu_pd(bb); - x3 = _mm256_loadu_pd(bb + 4); - x4 = _mm256_add_pd(x0, x2); - x5 = _mm256_add_pd(x1, x3); - _mm256_storeu_pd(rr, x4); - _mm256_storeu_pd(rr + 4, x5); - aa += 8; - bb += 8; - rr += 8; - } while (rr < rrend); -} - -void rnx_sub_avx(uint64_t nn, double* res, const double* a, const double* b) { - if (nn < 8) { - if (nn == 4) { - _mm256_storeu_pd(res, _mm256_sub_pd(_mm256_loadu_pd(a), _mm256_loadu_pd(b))); - } else if (nn == 2) { - _mm_storeu_pd(res, _mm_sub_pd(_mm_loadu_pd(a), _mm_loadu_pd(b))); - } else if (nn == 1) { - *res = *a - *b; - } else { - NOT_SUPPORTED(); // not a power of 2 - } - return; - } - // general case: nn >= 8 - __m256d x0, x1, x2, x3, x4, x5; - const double* aa = a; - const double* bb = b; - double* rr = res; - double* const rrend = res + nn; - do { - x0 = _mm256_loadu_pd(aa); - x1 = _mm256_loadu_pd(aa + 4); - x2 = _mm256_loadu_pd(bb); - x3 = _mm256_loadu_pd(bb + 4); - x4 = _mm256_sub_pd(x0, x2); - x5 = _mm256_sub_pd(x1, x3); - _mm256_storeu_pd(rr, x4); - _mm256_storeu_pd(rr + 4, x5); - aa += 8; - bb += 8; - rr += 8; - } while (rr < rrend); -} - -void rnx_negate_avx(uint64_t nn, double* res, const double* b) { - if (nn < 8) { - if (nn == 4) { - _mm256_storeu_pd(res, _mm256_sub_pd(_mm256_set1_pd(0), _mm256_loadu_pd(b))); - } else if (nn == 2) { - _mm_storeu_pd(res, _mm_sub_pd(_mm_set1_pd(0), _mm_loadu_pd(b))); - } else if (nn == 1) { - *res = -*b; - } else { - NOT_SUPPORTED(); // not a power of 2 - } - return; - } - // general case: nn >= 8 - __m256d x2, x3, x4, x5; - const __m256d ZERO = _mm256_set1_pd(0); - const double* bb = b; - double* rr = res; - double* const rrend = res + nn; - do { - x2 = _mm256_loadu_pd(bb); - x3 = _mm256_loadu_pd(bb + 4); - x4 = _mm256_sub_pd(ZERO, x2); - x5 = _mm256_sub_pd(ZERO, x3); - _mm256_storeu_pd(rr, x4); - _mm256_storeu_pd(rr + 4, x5); - bb += 8; - rr += 8; - } while (rr < rrend); -} - -/** @brief sets res = a + b */ -EXPORT void vec_rnx_add_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->n; - if (a_size < b_size) { - const uint64_t msize = res_size < a_size ? res_size : a_size; - const uint64_t nsize = res_size < b_size ? res_size : b_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_add_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - memcpy(res + i * res_sl, b + i * b_sl, nn * sizeof(double)); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } else { - const uint64_t msize = res_size < b_size ? res_size : b_size; - const uint64_t nsize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_add_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - memcpy(res + i * res_sl, a + i * a_sl, nn * sizeof(double)); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } -} - -/** @brief sets res = -a */ -EXPORT void vec_rnx_negate_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_negate_avx(nn, res + i * res_sl, a + i * a_sl); - } - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief sets res = a - b */ -EXPORT void vec_rnx_sub_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->n; - if (a_size < b_size) { - const uint64_t msize = res_size < a_size ? res_size : a_size; - const uint64_t nsize = res_size < b_size ? res_size : b_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_sub_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - rnx_negate_avx(nn, res + i * res_sl, b + i * b_sl); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } else { - const uint64_t msize = res_size < b_size ? res_size : b_size; - const uint64_t nsize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - rnx_sub_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - for (uint64_t i = msize; i < nsize; ++i) { - memcpy(res + i * res_sl, a + i * a_sl, nn * sizeof(double)); - } - for (uint64_t i = nsize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } - } -}
@@ -1,92 +0,0 @@
-#ifndef SPQLIOS_VEC_RNX_ARITHMETIC_PLUGIN_H -#define SPQLIOS_VEC_RNX_ARITHMETIC_PLUGIN_H - -#include "vec_rnx_arithmetic.h" - -typedef typeof(vec_rnx_zero) VEC_RNX_ZERO_F; -typedef typeof(vec_rnx_copy) VEC_RNX_COPY_F; -typedef typeof(vec_rnx_negate) VEC_RNX_NEGATE_F; -typedef typeof(vec_rnx_add) VEC_RNX_ADD_F; -typedef typeof(vec_rnx_sub) VEC_RNX_SUB_F; -typedef typeof(vec_rnx_rotate) VEC_RNX_ROTATE_F; -typedef typeof(vec_rnx_mul_xp_minus_one) VEC_RNX_MUL_XP_MINUS_ONE_F; -typedef typeof(vec_rnx_automorphism) VEC_RNX_AUTOMORPHISM_F; -typedef typeof(vec_rnx_to_znx32) VEC_RNX_TO_ZNX32_F; -typedef typeof(vec_rnx_from_znx32) VEC_RNX_FROM_ZNX32_F; -typedef typeof(vec_rnx_to_tnx32) VEC_RNX_TO_TNX32_F; -typedef typeof(vec_rnx_from_tnx32) VEC_RNX_FROM_TNX32_F; -typedef typeof(vec_rnx_to_tnx32x2) VEC_RNX_TO_TNX32X2_F; -typedef typeof(vec_rnx_from_tnx32x2) VEC_RNX_FROM_TNX32X2_F; -typedef typeof(vec_rnx_to_tnxdbl) VEC_RNX_TO_TNXDBL_F; -// typedef typeof(vec_rnx_from_tnxdbl) VEC_RNX_FROM_TNXDBL_F; -typedef typeof(rnx_small_single_product) RNX_SMALL_SINGLE_PRODUCT_F; -typedef typeof(rnx_small_single_product_tmp_bytes) RNX_SMALL_SINGLE_PRODUCT_TMP_BYTES_F; -typedef typeof(tnxdbl_small_single_product) TNXDBL_SMALL_SINGLE_PRODUCT_F; -typedef typeof(tnxdbl_small_single_product_tmp_bytes) TNXDBL_SMALL_SINGLE_PRODUCT_TMP_BYTES_F; -typedef typeof(znx32_small_single_product) ZNX32_SMALL_SINGLE_PRODUCT_F; -typedef typeof(znx32_small_single_product_tmp_bytes) ZNX32_SMALL_SINGLE_PRODUCT_TMP_BYTES_F; -typedef typeof(tnx32_small_single_product) TNX32_SMALL_SINGLE_PRODUCT_F; -typedef typeof(tnx32_small_single_product_tmp_bytes) TNX32_SMALL_SINGLE_PRODUCT_TMP_BYTES_F; -typedef typeof(rnx_approxdecomp_from_tnx32) RNX_APPROXDECOMP_FROM_TNX32_F; -typedef typeof(rnx_approxdecomp_from_tnx32x2) RNX_APPROXDECOMP_FROM_TNX32X2_F; -typedef typeof(rnx_approxdecomp_from_tnxdbl) RNX_APPROXDECOMP_FROM_TNXDBL_F; -typedef typeof(bytes_of_rnx_svp_ppol) BYTES_OF_RNX_SVP_PPOL_F; -typedef typeof(rnx_svp_prepare) RNX_SVP_PREPARE_F; -typedef typeof(rnx_svp_apply) RNX_SVP_APPLY_F; -typedef typeof(bytes_of_rnx_vmp_pmat) BYTES_OF_RNX_VMP_PMAT_F; -typedef typeof(rnx_vmp_prepare_contiguous) RNX_VMP_PREPARE_CONTIGUOUS_F; -typedef typeof(rnx_vmp_prepare_dblptr) RNX_VMP_PREPARE_DBLPTR_F; -typedef typeof(rnx_vmp_prepare_row) RNX_VMP_PREPARE_ROW_F; -typedef typeof(rnx_vmp_prepare_tmp_bytes) RNX_VMP_PREPARE_TMP_BYTES_F; -typedef typeof(rnx_vmp_apply_tmp_a) RNX_VMP_APPLY_TMP_A_F; -typedef typeof(rnx_vmp_apply_tmp_a_tmp_bytes) RNX_VMP_APPLY_TMP_A_TMP_BYTES_F; -typedef typeof(rnx_vmp_apply_dft_to_dft) RNX_VMP_APPLY_DFT_TO_DFT_F; -typedef typeof(rnx_vmp_apply_dft_to_dft_tmp_bytes) RNX_VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F; -typedef typeof(vec_rnx_dft) VEC_RNX_DFT_F; -typedef typeof(vec_rnx_idft) VEC_RNX_IDFT_F; - -typedef struct rnx_module_vtable_t RNX_MODULE_VTABLE; -struct rnx_module_vtable_t { - VEC_RNX_ZERO_F* vec_rnx_zero; - VEC_RNX_COPY_F* vec_rnx_copy; - VEC_RNX_NEGATE_F* vec_rnx_negate; - VEC_RNX_ADD_F* vec_rnx_add; - VEC_RNX_SUB_F* vec_rnx_sub; - VEC_RNX_ROTATE_F* vec_rnx_rotate; - VEC_RNX_MUL_XP_MINUS_ONE_F* vec_rnx_mul_xp_minus_one; - VEC_RNX_AUTOMORPHISM_F* vec_rnx_automorphism; - VEC_RNX_TO_ZNX32_F* vec_rnx_to_znx32; - VEC_RNX_FROM_ZNX32_F* vec_rnx_from_znx32; - VEC_RNX_TO_TNX32_F* vec_rnx_to_tnx32; - VEC_RNX_FROM_TNX32_F* vec_rnx_from_tnx32; - VEC_RNX_TO_TNX32X2_F* vec_rnx_to_tnx32x2; - VEC_RNX_FROM_TNX32X2_F* vec_rnx_from_tnx32x2; - VEC_RNX_TO_TNXDBL_F* vec_rnx_to_tnxdbl; - RNX_SMALL_SINGLE_PRODUCT_F* rnx_small_single_product; - RNX_SMALL_SINGLE_PRODUCT_TMP_BYTES_F* rnx_small_single_product_tmp_bytes; - TNXDBL_SMALL_SINGLE_PRODUCT_F* tnxdbl_small_single_product; - TNXDBL_SMALL_SINGLE_PRODUCT_TMP_BYTES_F* tnxdbl_small_single_product_tmp_bytes; - ZNX32_SMALL_SINGLE_PRODUCT_F* znx32_small_single_product; - ZNX32_SMALL_SINGLE_PRODUCT_TMP_BYTES_F* znx32_small_single_product_tmp_bytes; - TNX32_SMALL_SINGLE_PRODUCT_F* tnx32_small_single_product; - TNX32_SMALL_SINGLE_PRODUCT_TMP_BYTES_F* tnx32_small_single_product_tmp_bytes; - RNX_APPROXDECOMP_FROM_TNX32_F* rnx_approxdecomp_from_tnx32; - RNX_APPROXDECOMP_FROM_TNX32X2_F* rnx_approxdecomp_from_tnx32x2; - RNX_APPROXDECOMP_FROM_TNXDBL_F* rnx_approxdecomp_from_tnxdbl; - BYTES_OF_RNX_SVP_PPOL_F* bytes_of_rnx_svp_ppol; - RNX_SVP_PREPARE_F* rnx_svp_prepare; - RNX_SVP_APPLY_F* rnx_svp_apply; - BYTES_OF_RNX_VMP_PMAT_F* bytes_of_rnx_vmp_pmat; - RNX_VMP_PREPARE_CONTIGUOUS_F* rnx_vmp_prepare_contiguous; - RNX_VMP_PREPARE_DBLPTR_F* rnx_vmp_prepare_dblptr; - RNX_VMP_PREPARE_ROW_F* rnx_vmp_prepare_row; - RNX_VMP_PREPARE_TMP_BYTES_F* rnx_vmp_prepare_tmp_bytes; - RNX_VMP_APPLY_TMP_A_F* rnx_vmp_apply_tmp_a; - RNX_VMP_APPLY_TMP_A_TMP_BYTES_F* rnx_vmp_apply_tmp_a_tmp_bytes; - RNX_VMP_APPLY_DFT_TO_DFT_F* rnx_vmp_apply_dft_to_dft; - RNX_VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* rnx_vmp_apply_dft_to_dft_tmp_bytes; - VEC_RNX_DFT_F* vec_rnx_dft; - VEC_RNX_IDFT_F* vec_rnx_idft; -}; - -#endif // SPQLIOS_VEC_RNX_ARITHMETIC_PLUGIN_H
@@ -1,309 +0,0 @@
-#ifndef SPQLIOS_VEC_RNX_ARITHMETIC_PRIVATE_H -#define SPQLIOS_VEC_RNX_ARITHMETIC_PRIVATE_H - -#include "../commons_private.h" -#include "../reim/reim_fft.h" -#include "vec_rnx_arithmetic.h" -#include "vec_rnx_arithmetic_plugin.h" - -typedef struct fft64_rnx_module_precomp_t FFT64_RNX_MODULE_PRECOMP; -struct fft64_rnx_module_precomp_t { - REIM_FFT_PRECOMP* p_fft; - REIM_IFFT_PRECOMP* p_ifft; - REIM_FFTVEC_ADD_PRECOMP* p_fftvec_add; - REIM_FFTVEC_MUL_PRECOMP* p_fftvec_mul; - REIM_FFTVEC_ADDMUL_PRECOMP* p_fftvec_addmul; -}; - -typedef union rnx_module_precomp_t RNX_MODULE_PRECOMP; -union rnx_module_precomp_t { - FFT64_RNX_MODULE_PRECOMP fft64; -}; - -void fft64_init_rnx_module_precomp(MOD_RNX* module); - -void fft64_finalize_rnx_module_precomp(MOD_RNX* module); - -/** @brief opaque structure that describes the modules (RnX,ZnX,TnX) and the hardware */ -struct rnx_module_info_t { - uint64_t n; - uint64_t m; - RNX_MODULE_TYPE mtype; - RNX_MODULE_VTABLE vtable; - RNX_MODULE_PRECOMP precomp; - void* custom; - void (*custom_deleter)(void*); -}; - -void init_rnx_module_info(MOD_RNX* module, // - uint64_t, RNX_MODULE_TYPE mtype); - -void finalize_rnx_module_info(MOD_RNX* module); - -void fft64_init_rnx_module_vtable(MOD_RNX* module); - -/////////////////////////////////////////////////////////////////// -// prepared gadget decompositions (optimized) // -/////////////////////////////////////////////////////////////////// - -struct tnx32_approxdec_gadget_t { - uint64_t k; - uint64_t ell; - int32_t add_cst; // 1/2.(sum 2^-(i+1)K) - int32_t rshift_base; // 32 - K - int64_t and_mask; // 2^K-1 - int64_t or_mask; // double(2^52) - double sub_cst; // double(2^52 + 2^(K-1)) - uint8_t rshifts[8]; // 32 - (i+1).K -}; - -struct tnx32x2_approxdec_gadget_t { - // TODO -}; - -struct tnxdbl_approxdecomp_gadget_t { - uint64_t k; - uint64_t ell; - double add_cst; // double(3.2^(51-ell.K) + 1/2.(sum 2^(-iK)) for i=[0,ell[) - uint64_t and_mask; // uint64(2^(K)-1) - uint64_t or_mask; // double(2^52) - double sub_cst; // double(2^52 + 2^(K-1)) -}; - -EXPORT void vec_rnx_add_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -); -EXPORT void vec_rnx_add_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = 0 */ -EXPORT void vec_rnx_zero_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl // res -); - -/** @brief sets res = a */ -EXPORT void vec_rnx_copy_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = -a */ -EXPORT void vec_rnx_negate_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = -a */ -EXPORT void vec_rnx_negate_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a - b */ -EXPORT void vec_rnx_sub_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a - b */ -EXPORT void vec_rnx_sub_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl, // a - const double* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a . X^p */ -EXPORT void vec_rnx_rotate_ref( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a(X^p) */ -EXPORT void vec_rnx_automorphism_ref( // - const MOD_RNX* module, // N - int64_t p, // X -> X^p - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief number of bytes in a RNX_VMP_PMAT (for manual allocation) */ -EXPORT uint64_t fft64_bytes_of_rnx_vmp_pmat(const MOD_RNX* module, // N - uint64_t nrows, uint64_t ncols); - -EXPORT void fft64_rnx_vmp_apply_dft_to_dft_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl, // a - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); -EXPORT void fft64_rnx_vmp_apply_dft_to_dft_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl, // a - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); -EXPORT uint64_t fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); -EXPORT uint64_t fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_avx( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); -EXPORT void fft64_rnx_vmp_prepare_contiguous_ref( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); -EXPORT void fft64_rnx_vmp_prepare_contiguous_avx( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); -EXPORT void fft64_rnx_vmp_prepare_dblptr_ref( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double** mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); -EXPORT void fft64_rnx_vmp_prepare_dblptr_avx( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double** mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); -EXPORT void fft64_rnx_vmp_prepare_row_ref( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* mat, uint64_t row_i, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); -EXPORT void fft64_rnx_vmp_prepare_row_avx( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* mat, uint64_t row_i, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); -EXPORT uint64_t fft64_rnx_vmp_prepare_tmp_bytes_ref(const MOD_RNX* module); -EXPORT uint64_t fft64_rnx_vmp_prepare_tmp_bytes_avx(const MOD_RNX* module); - -EXPORT void fft64_rnx_vmp_apply_tmp_a_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res (addr must be != a) - double* tmpa, uint64_t a_size, uint64_t a_sl, // a (will be overwritten) - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -); -EXPORT void fft64_rnx_vmp_apply_tmp_a_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res (addr must be != a) - double* tmpa, uint64_t a_size, uint64_t a_sl, // a (will be overwritten) - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -); - -EXPORT uint64_t fft64_rnx_vmp_apply_tmp_a_tmp_bytes_ref( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); -EXPORT uint64_t fft64_rnx_vmp_apply_tmp_a_tmp_bytes_avx( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); - -/// gadget decompositions - -/** @brief sets res = gadget_decompose(a) */ -EXPORT void rnx_approxdecomp_from_tnxdbl_ref( // - const MOD_RNX* module, // N - const TNXDBL_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a); // a -EXPORT void rnx_approxdecomp_from_tnxdbl_avx( // - const MOD_RNX* module, // N - const TNXDBL_APPROXDECOMP_GADGET* gadget, // output base 2^K - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a); // a - -EXPORT void vec_rnx_mul_xp_minus_one_ref( // - const MOD_RNX* module, // N - const int64_t p, // rotation value - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_to_znx32_ref( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_from_znx32_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_to_tnx32_ref( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_from_tnx32_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_rnx_to_tnxdbl_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT uint64_t fft64_bytes_of_rnx_svp_ppol(const MOD_RNX* module); // N - -/** @brief prepares a svp polynomial */ -EXPORT void fft64_rnx_svp_prepare_ref(const MOD_RNX* module, // N - RNX_SVP_PPOL* ppol, // output - const double* pol // a -); - -/** @brief apply a svp product, result = ppol * a, presented in DFT space */ -EXPORT void fft64_rnx_svp_apply_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // output - const RNX_SVP_PPOL* ppol, // prepared pol - const double* a, uint64_t a_size, uint64_t a_sl // a -); - -#endif // SPQLIOS_VEC_RNX_ARITHMETIC_PRIVATE_H
@@ -1,91 +0,0 @@
-#include <memory.h> - -#include "vec_rnx_arithmetic_private.h" -#include "zn_arithmetic_private.h" - -EXPORT void vec_rnx_to_znx32_ref( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - dbl_round_to_i32_ref(NULL, res + i * res_sl, nn, a + i * a_sl, nn); - } - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(int32_t)); - } -} - -EXPORT void vec_rnx_from_znx32_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - i32_to_dbl_ref(NULL, res + i * res_sl, nn, a + i * a_sl, nn); - } - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(int32_t)); - } -} -EXPORT void vec_rnx_to_tnx32_ref( // - const MOD_RNX* module, // N - int32_t* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - dbl_to_tn32_ref(NULL, res + i * res_sl, nn, a + i * a_sl, nn); - } - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(int32_t)); - } -} -EXPORT void vec_rnx_from_tnx32_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const int32_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - tn32_to_dbl_ref(NULL, res + i * res_sl, nn, a + i * a_sl, nn); - } - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(int32_t)); - } -} - -static void dbl_to_tndbl_ref( // - const void* UNUSED, // N - double* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - static const double OFF_CST = INT64_C(3) << 51; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - double ai = a[i] + OFF_CST; - res[i] = a[i] - (ai - OFF_CST); - } - memset(res + msize, 0, (res_size - msize) * sizeof(double)); -} - -EXPORT void vec_rnx_to_tnxdbl_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - dbl_to_tndbl_ref(NULL, res + i * res_sl, nn, a + i * a_sl, nn); - } - for (uint64_t i = msize; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(int32_t)); - } -}
@@ -1,47 +0,0 @@
-#include <string.h> - -#include "../coeffs/coeffs_arithmetic.h" -#include "vec_rnx_arithmetic_private.h" - -EXPORT uint64_t fft64_bytes_of_rnx_svp_ppol(const MOD_RNX* module) { return module->n * sizeof(double); } - -EXPORT RNX_SVP_PPOL* new_rnx_svp_ppol(const MOD_RNX* module) { return spqlios_alloc(bytes_of_rnx_svp_ppol(module)); } - -EXPORT void delete_rnx_svp_ppol(RNX_SVP_PPOL* ppol) { spqlios_free(ppol); } - -/** @brief prepares a svp polynomial */ -EXPORT void fft64_rnx_svp_prepare_ref(const MOD_RNX* module, // N - RNX_SVP_PPOL* ppol, // output - const double* pol // a -) { - double* const dppol = (double*)ppol; - rnx_divide_by_m_ref(module->n, module->m, dppol, pol); - reim_fft(module->precomp.fft64.p_fft, dppol); -} - -EXPORT void fft64_rnx_svp_apply_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // output - const RNX_SVP_PPOL* ppol, // prepared pol - const double* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->n; - double* const dppol = (double*)ppol; - - const uint64_t auto_end_idx = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < auto_end_idx; ++i) { - const double* a_ptr = a + i * a_sl; - double* const res_ptr = res + i * res_sl; - // copy the polynomial to res, apply fft in place, call fftvec - // _mul, apply ifft in place. - memcpy(res_ptr, a_ptr, nn * sizeof(double)); - reim_fft(module->precomp.fft64.p_fft, (double*)res_ptr); - reim_fftvec_mul(module->precomp.fft64.p_fftvec_mul, res_ptr, res_ptr, dppol); - reim_ifft(module->precomp.fft64.p_ifft, res_ptr); - } - - // then extend with zeros - for (uint64_t i = auto_end_idx; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -}
@@ -1,254 +0,0 @@
-#include <assert.h> -#include <immintrin.h> -#include <string.h> - -#include "../coeffs/coeffs_arithmetic.h" -#include "../reim/reim_fft.h" -#include "../reim4/reim4_arithmetic.h" -#include "vec_rnx_arithmetic_private.h" - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void fft64_rnx_vmp_prepare_contiguous_avx( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - // there is an edge case if nn < 8 - const uint64_t nn = module->n; - const uint64_t m = module->m; - - double* const dtmp = (double*)tmp_space; - double* const output_mat = (double*)pmat; - double* start_addr = (double*)pmat; - uint64_t offset = nrows * ncols * 8; - - if (nn >= 8) { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - rnx_divide_by_m_avx(nn, m, dtmp, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->precomp.fft64.p_fft, dtmp); - - if (col_i == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - start_addr = output_mat + col_i * nrows * 8 // col == ncols-1 - + row_i * 8; - } else { - // general case: columns go by pair - start_addr = output_mat + (col_i / 2) * (2 * nrows) * 8 // second: col pair index - + row_i * 2 * 8 // third: row index - + (col_i % 2) * 8; - } - - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - // extract blk from tmp and save it - reim4_extract_1blk_from_reim_avx(m, blk_i, start_addr + blk_i * offset, dtmp); - } - } - } - } else { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - double* res = output_mat + (col_i * nrows + row_i) * nn; - rnx_divide_by_m_avx(nn, m, res, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->precomp.fft64.p_fft, res); - } - } - } -} - -/** @brief prepares a vmp matrix (mat[row]+col*N points to the item) */ -EXPORT void fft64_rnx_vmp_prepare_dblptr_avx( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double** mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - fft64_rnx_vmp_prepare_row_avx(module, pmat, mat[row_i], row_i, nrows, ncols, tmp_space); - } -} - -/** @brief prepares the ith-row of a vmp matrix with nrows and ncols */ -EXPORT void fft64_rnx_vmp_prepare_row_avx( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* row, uint64_t row_i, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - // there is an edge case if nn < 8 - const uint64_t nn = module->n; - const uint64_t m = module->m; - - double* const dtmp = (double*)tmp_space; - double* const output_mat = (double*)pmat; - double* start_addr = (double*)pmat; - uint64_t offset = nrows * ncols * 8; - - if (nn >= 8) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - rnx_divide_by_m_avx(nn, m, dtmp, row + col_i * nn); - reim_fft(module->precomp.fft64.p_fft, dtmp); - - if (col_i == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - start_addr = output_mat + col_i * nrows * 8 // col == ncols-1 - + row_i * 8; - } else { - // general case: columns go by pair - start_addr = output_mat + (col_i / 2) * (2 * nrows) * 8 // second: col pair index - + row_i * 2 * 8 // third: row index - + (col_i % 2) * 8; - } - - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - // extract blk from tmp and save it - reim4_extract_1blk_from_reim_avx(m, blk_i, start_addr + blk_i * offset, dtmp); - } - } - } else { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - double* res = output_mat + (col_i * nrows + row_i) * nn; - rnx_divide_by_m_avx(nn, m, res, row + col_i * nn); - reim_fft(module->precomp.fft64.p_fft, res); - } - } -} - -/** @brief minimal size of the tmp_space */ -EXPORT void fft64_rnx_vmp_apply_dft_to_dft_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl, // a - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - const uint64_t m = module->m; - const uint64_t nn = module->n; - - double* mat2cols_output = (double*)tmp_space; // 128 bytes - double* extracted_blk = (double*)tmp_space + 16; // 64*min(nrows,a_size) bytes - - double* mat_input = (double*)pmat; - - const uint64_t row_max = nrows < a_size ? nrows : a_size; - const uint64_t col_max = ncols < res_size ? ncols : res_size; - - if (row_max > 0 && col_max > 0) { - if (nn >= 8) { - // let's do some prefetching of the GSW key, since on some cpus, - // it helps - const uint64_t ms4 = m >> 2; // m/4 - const uint64_t gsw_iter_doubles = 8 * nrows * ncols; - const uint64_t pref_doubles = 1200; - const double* gsw_pref_ptr = mat_input; - const double* const gsw_ptr_end = mat_input + ms4 * gsw_iter_doubles; - const double* gsw_pref_ptr_target = mat_input + pref_doubles; - for (; gsw_pref_ptr < gsw_pref_ptr_target; gsw_pref_ptr += 8) { - __builtin_prefetch(gsw_pref_ptr, 0, _MM_HINT_T0); - } - const double* mat_blk_start; - uint64_t blk_i; - for (blk_i = 0, mat_blk_start = mat_input; blk_i < ms4; blk_i++, mat_blk_start += gsw_iter_doubles) { - // prefetch the next iteration - if (gsw_pref_ptr_target < gsw_ptr_end) { - gsw_pref_ptr_target += gsw_iter_doubles; - if (gsw_pref_ptr_target > gsw_ptr_end) gsw_pref_ptr_target = gsw_ptr_end; - for (; gsw_pref_ptr < gsw_pref_ptr_target; gsw_pref_ptr += 8) { - __builtin_prefetch(gsw_pref_ptr, 0, _MM_HINT_T0); - } - } - reim4_extract_1blk_from_contiguous_reim_sl_avx(m, a_sl, row_max, blk_i, extracted_blk, a_dft); - // apply mat2cols - for (uint64_t col_i = 0; col_i < col_max - 1; col_i += 2) { - uint64_t col_offset = col_i * (8 * nrows); - reim4_vec_mat2cols_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - - reim4_save_1blk_to_reim_avx(m, blk_i, res + col_i * res_sl, mat2cols_output); - reim4_save_1blk_to_reim_avx(m, blk_i, res + (col_i + 1) * res_sl, mat2cols_output + 8); - } - - // check if col_max is odd, then special case - if (col_max % 2 == 1) { - uint64_t last_col = col_max - 1; - uint64_t col_offset = last_col * (8 * nrows); - - // the last column is alone in the pmat: vec_mat1col - if (ncols == col_max) { - reim4_vec_mat1col_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } else { - // the last column is part of a colpair in the pmat: vec_mat2cols and ignore the second position - reim4_vec_mat2cols_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } - reim4_save_1blk_to_reim_avx(m, blk_i, res + last_col * res_sl, mat2cols_output); - } - } - } else { - const double* in; - uint64_t in_sl; - if (res == a_dft) { - // it is in place: copy the input vector - in = (double*)tmp_space; - in_sl = nn; - // vec_rnx_copy(module, (double*)tmp_space, row_max, nn, a_dft, row_max, a_sl); - for (uint64_t row_i = 0; row_i < row_max; row_i++) { - memcpy((double*)tmp_space + row_i * nn, a_dft + row_i * a_sl, nn * sizeof(double)); - } - } else { - // it is out of place: do the product directly - in = a_dft; - in_sl = a_sl; - } - for (uint64_t col_i = 0; col_i < col_max; col_i++) { - double* pmat_col = mat_input + col_i * nrows * nn; - { - reim_fftvec_mul(module->precomp.fft64.p_fftvec_mul, // - res + col_i * res_sl, // - in, // - pmat_col); - } - for (uint64_t row_i = 1; row_i < row_max; row_i++) { - reim_fftvec_addmul(module->precomp.fft64.p_fftvec_addmul, // - res + col_i * res_sl, // - in + row_i * in_sl, // - pmat_col + row_i * nn); - } - } - } - } - // zero out remaining bytes (if any) - for (uint64_t i = col_max; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief applies a vmp product res = a x pmat */ -EXPORT void fft64_rnx_vmp_apply_tmp_a_avx( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res (addr must be != a) - double* tmpa, uint64_t a_size, uint64_t a_sl, // a (will be overwritten) - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -) { - const uint64_t nn = module->n; - const uint64_t rows = nrows < a_size ? nrows : a_size; - const uint64_t cols = ncols < res_size ? ncols : res_size; - - // fft is done in place on the input (tmpa is destroyed) - for (uint64_t i = 0; i < rows; ++i) { - reim_fft(module->precomp.fft64.p_fft, tmpa + i * a_sl); - } - fft64_rnx_vmp_apply_dft_to_dft_avx(module, // - res, cols, res_sl, // - tmpa, rows, a_sl, // - pmat, nrows, ncols, // - tmp_space); - // ifft is done in place on the output - for (uint64_t i = 0; i < cols; ++i) { - reim_ifft(module->precomp.fft64.p_ifft, res + i * res_sl); - } - // zero out the remaining positions - for (uint64_t i = cols; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -}
@@ -1,309 +0,0 @@
-#include <assert.h> -#include <string.h> - -#include "../coeffs/coeffs_arithmetic.h" -#include "../reim/reim_fft.h" -#include "../reim4/reim4_arithmetic.h" -#include "vec_rnx_arithmetic_private.h" - -/** @brief number of bytes in a RNX_VMP_PMAT (for manual allocation) */ -EXPORT uint64_t fft64_bytes_of_rnx_vmp_pmat(const MOD_RNX* module, // N - uint64_t nrows, uint64_t ncols) { // dimensions - return nrows * ncols * module->n * sizeof(double); -} - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void fft64_rnx_vmp_prepare_contiguous_ref( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - // there is an edge case if nn < 8 - const uint64_t nn = module->n; - const uint64_t m = module->m; - - double* const dtmp = (double*)tmp_space; - double* const output_mat = (double*)pmat; - double* start_addr = (double*)pmat; - uint64_t offset = nrows * ncols * 8; - - if (nn >= 8) { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - rnx_divide_by_m_ref(nn, m, dtmp, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->precomp.fft64.p_fft, dtmp); - - if (col_i == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - start_addr = output_mat + col_i * nrows * 8 // col == ncols-1 - + row_i * 8; - } else { - // general case: columns go by pair - start_addr = output_mat + (col_i / 2) * (2 * nrows) * 8 // second: col pair index - + row_i * 2 * 8 // third: row index - + (col_i % 2) * 8; - } - - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - // extract blk from tmp and save it - reim4_extract_1blk_from_reim_ref(m, blk_i, start_addr + blk_i * offset, dtmp); - } - } - } - } else { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - double* res = output_mat + (col_i * nrows + row_i) * nn; - rnx_divide_by_m_ref(nn, m, res, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->precomp.fft64.p_fft, res); - } - } - } -} - -/** @brief prepares a vmp matrix (mat[row]+col*N points to the item) */ -EXPORT void fft64_rnx_vmp_prepare_dblptr_ref( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double** mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - fft64_rnx_vmp_prepare_row_ref(module, pmat, mat[row_i], row_i, nrows, ncols, tmp_space); - } -} - -/** @brief prepares the ith-row of a vmp matrix with nrows and ncols */ -EXPORT void fft64_rnx_vmp_prepare_row_ref( // - const MOD_RNX* module, // N - RNX_VMP_PMAT* pmat, // output - const double* row, uint64_t row_i, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - // there is an edge case if nn < 8 - const uint64_t nn = module->n; - const uint64_t m = module->m; - - double* const dtmp = (double*)tmp_space; - double* const output_mat = (double*)pmat; - double* start_addr = (double*)pmat; - uint64_t offset = nrows * ncols * 8; - - if (nn >= 8) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - rnx_divide_by_m_ref(nn, m, dtmp, row + col_i * nn); - reim_fft(module->precomp.fft64.p_fft, dtmp); - - if (col_i == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - start_addr = output_mat + col_i * nrows * 8 // col == ncols-1 - + row_i * 8; - } else { - // general case: columns go by pair - start_addr = output_mat + (col_i / 2) * (2 * nrows) * 8 // second: col pair index - + row_i * 2 * 8 // third: row index - + (col_i % 2) * 8; - } - - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - // extract blk from tmp and save it - reim4_extract_1blk_from_reim_ref(m, blk_i, start_addr + blk_i * offset, dtmp); - } - } - } else { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - double* res = output_mat + (col_i * nrows + row_i) * nn; - rnx_divide_by_m_ref(nn, m, res, row + col_i * nn); - reim_fft(module->precomp.fft64.p_fft, res); - } - } -} - -/** @brief number of scratch bytes necessary to prepare a matrix */ -EXPORT uint64_t fft64_rnx_vmp_prepare_tmp_bytes_ref(const MOD_RNX* module) { - const uint64_t nn = module->n; - return nn * sizeof(int64_t); -} - -/** @brief minimal size of the tmp_space */ -EXPORT void fft64_rnx_vmp_apply_dft_to_dft_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res - const double* a_dft, uint64_t a_size, uint64_t a_sl, // a - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - const uint64_t m = module->m; - const uint64_t nn = module->n; - - double* mat2cols_output = (double*)tmp_space; // 128 bytes - double* extracted_blk = (double*)tmp_space + 16; // 64*min(nrows,a_size) bytes - - double* mat_input = (double*)pmat; - - const uint64_t row_max = nrows < a_size ? nrows : a_size; - const uint64_t col_max = ncols < res_size ? ncols : res_size; - - if (row_max > 0 && col_max > 0) { - if (nn >= 8) { - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - double* mat_blk_start = mat_input + blk_i * (8 * nrows * ncols); - - reim4_extract_1blk_from_contiguous_reim_sl_ref(m, a_sl, row_max, blk_i, extracted_blk, a_dft); - // apply mat2cols - for (uint64_t col_i = 0; col_i < col_max - 1; col_i += 2) { - uint64_t col_offset = col_i * (8 * nrows); - reim4_vec_mat2cols_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - - reim4_save_1blk_to_reim_ref(m, blk_i, res + col_i * res_sl, mat2cols_output); - reim4_save_1blk_to_reim_ref(m, blk_i, res + (col_i + 1) * res_sl, mat2cols_output + 8); - } - - // check if col_max is odd, then special case - if (col_max % 2 == 1) { - uint64_t last_col = col_max - 1; - uint64_t col_offset = last_col * (8 * nrows); - - // the last column is alone in the pmat: vec_mat1col - if (ncols == col_max) { - reim4_vec_mat1col_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } else { - // the last column is part of a colpair in the pmat: vec_mat2cols and ignore the second position - reim4_vec_mat2cols_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } - reim4_save_1blk_to_reim_ref(m, blk_i, res + last_col * res_sl, mat2cols_output); - } - } - } else { - const double* in; - uint64_t in_sl; - if (res == a_dft) { - // it is in place: copy the input vector - in = (double*)tmp_space; - in_sl = nn; - // vec_rnx_copy(module, (double*)tmp_space, row_max, nn, a_dft, row_max, a_sl); - for (uint64_t row_i = 0; row_i < row_max; row_i++) { - memcpy((double*)tmp_space + row_i * nn, a_dft + row_i * a_sl, nn * sizeof(double)); - } - } else { - // it is out of place: do the product directly - in = a_dft; - in_sl = a_sl; - } - for (uint64_t col_i = 0; col_i < col_max; col_i++) { - double* pmat_col = mat_input + col_i * nrows * nn; - { - reim_fftvec_mul(module->precomp.fft64.p_fftvec_mul, // - res + col_i * res_sl, // - in, // - pmat_col); - } - for (uint64_t row_i = 1; row_i < row_max; row_i++) { - reim_fftvec_addmul(module->precomp.fft64.p_fftvec_addmul, // - res + col_i * res_sl, // - in + row_i * in_sl, // - pmat_col + row_i * nn); - } - } - } - } - // zero out remaining bytes (if any) - for (uint64_t i = col_max; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief applies a vmp product res = a x pmat */ -EXPORT void fft64_rnx_vmp_apply_tmp_a_ref( // - const MOD_RNX* module, // N - double* res, uint64_t res_size, uint64_t res_sl, // res (addr must be != a) - double* tmpa, uint64_t a_size, uint64_t a_sl, // a (will be overwritten) - const RNX_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -) { - const uint64_t nn = module->n; - const uint64_t rows = nrows < a_size ? nrows : a_size; - const uint64_t cols = ncols < res_size ? ncols : res_size; - - // fft is done in place on the input (tmpa is destroyed) - for (uint64_t i = 0; i < rows; ++i) { - reim_fft(module->precomp.fft64.p_fft, tmpa + i * a_sl); - } - fft64_rnx_vmp_apply_dft_to_dft_ref(module, // - res, cols, res_sl, // - tmpa, rows, a_sl, // - pmat, nrows, ncols, // - tmp_space); - // ifft is done in place on the output - for (uint64_t i = 0; i < cols; ++i) { - reim_ifft(module->precomp.fft64.p_ifft, res + i * res_sl); - } - // zero out the remaining positions - for (uint64_t i = cols; i < res_size; ++i) { - memset(res + i * res_sl, 0, nn * sizeof(double)); - } -} - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - const uint64_t row_max = nrows < a_size ? nrows : a_size; - - return (128) + (64 * row_max); -} - -#ifdef __APPLE__ -EXPORT uint64_t fft64_rnx_vmp_apply_tmp_a_tmp_bytes_ref( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - return fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref(module, res_size, a_size, nrows, ncols); -} -#else -EXPORT uint64_t fft64_rnx_vmp_apply_tmp_a_tmp_bytes_ref( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix - ) __attribute((alias("fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref"))); -#endif -// avx aliases that need to be defined in the same .c file - -/** @brief number of scratch bytes necessary to prepare a matrix */ -#ifdef __APPLE__ -#pragma weak fft64_rnx_vmp_prepare_tmp_bytes_avx = fft64_rnx_vmp_prepare_tmp_bytes_ref -#else -EXPORT uint64_t fft64_rnx_vmp_prepare_tmp_bytes_avx(const MOD_RNX* module) - __attribute((alias("fft64_rnx_vmp_prepare_tmp_bytes_ref"))); -#endif - -/** @brief minimal size of the tmp_space */ -#ifdef __APPLE__ -#pragma weak fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_avx = fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref -#else -EXPORT uint64_t fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_avx( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix - ) __attribute((alias("fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref"))); -#endif - -#ifdef __APPLE__ -#pragma weak fft64_rnx_vmp_apply_tmp_a_tmp_bytes_avx = fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref -#else -EXPORT uint64_t fft64_rnx_vmp_apply_tmp_a_tmp_bytes_avx( // - const MOD_RNX* module, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix - ) __attribute((alias("fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref"))); -#endif -// wrappers
@@ -1,369 +0,0 @@
-#include <assert.h> -#include <math.h> -#include <stdint.h> -#include <stdlib.h> -#include <string.h> - -#include "../coeffs/coeffs_arithmetic.h" -#include "../q120/q120_arithmetic.h" -#include "../q120/q120_ntt.h" -#include "../reim/reim_fft_internal.h" -#include "../reim4/reim4_arithmetic.h" -#include "vec_znx_arithmetic.h" -#include "vec_znx_arithmetic_private.h" - -// general function (virtual dispatch) - -EXPORT void vec_znx_add(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - module->func.vec_znx_add(module, // N - res, res_size, res_sl, // res - a, a_size, a_sl, // a - b, b_size, b_sl // b - ); -} - -EXPORT void vec_znx_sub(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - module->func.vec_znx_sub(module, // N - res, res_size, res_sl, // res - a, a_size, a_sl, // a - b, b_size, b_sl // b - ); -} - -EXPORT void vec_znx_rotate(const MODULE* module, // N - const int64_t p, // rotation value - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->func.vec_znx_rotate(module, // N - p, // p - res, res_size, res_sl, // res - a, a_size, a_sl // a - ); -} - -EXPORT void vec_znx_mul_xp_minus_one(const MODULE* module, // N - const int64_t p, // p - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->func.vec_znx_mul_xp_minus_one(module, // N - p, // p - res, res_size, res_sl, // res - a, a_size, a_sl // a - ); -} - -EXPORT void vec_znx_automorphism(const MODULE* module, // N - const int64_t p, // X->X^p - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->func.vec_znx_automorphism(module, // N - p, // p - res, res_size, res_sl, // res - a, a_size, a_sl // a - ); -} - -EXPORT void vec_znx_normalize_base2k(const MODULE* module, uint64_t nn, // N - uint64_t log2_base2k, // output base 2^K - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - uint8_t* tmp_space // scratch space of size >= N -) { - module->func.vec_znx_normalize_base2k(module, nn, // N - log2_base2k, // log2_base2k - res, res_size, res_sl, // res - a, a_size, a_sl, // a - tmp_space); -} - -EXPORT uint64_t vec_znx_normalize_base2k_tmp_bytes(const MODULE* module, uint64_t nn // N -) { - return module->func.vec_znx_normalize_base2k_tmp_bytes(module, nn // N - ); -} - -// specialized function (ref) - -EXPORT void vec_znx_add_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->nn; - if (a_size <= b_size) { - const uint64_t sum_idx = res_size < a_size ? res_size : a_size; - const uint64_t copy_idx = res_size < b_size ? res_size : b_size; - // add up to the smallest dimension - for (uint64_t i = 0; i < sum_idx; ++i) { - znx_add_i64_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then copy to the largest dimension - for (uint64_t i = sum_idx; i < copy_idx; ++i) { - znx_copy_i64_ref(nn, res + i * res_sl, b + i * b_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } - } else { - const uint64_t sum_idx = res_size < b_size ? res_size : b_size; - const uint64_t copy_idx = res_size < a_size ? res_size : a_size; - // add up to the smallest dimension - for (uint64_t i = 0; i < sum_idx; ++i) { - znx_add_i64_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then copy to the largest dimension - for (uint64_t i = sum_idx; i < copy_idx; ++i) { - znx_copy_i64_ref(nn, res + i * res_sl, a + i * a_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } - } -} - -EXPORT void vec_znx_sub_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->nn; - if (a_size <= b_size) { - const uint64_t sub_idx = res_size < a_size ? res_size : a_size; - const uint64_t copy_idx = res_size < b_size ? res_size : b_size; - // subtract up to the smallest dimension - for (uint64_t i = 0; i < sub_idx; ++i) { - znx_sub_i64_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then negate to the largest dimension - for (uint64_t i = sub_idx; i < copy_idx; ++i) { - znx_negate_i64_ref(nn, res + i * res_sl, b + i * b_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } - } else { - const uint64_t sub_idx = res_size < b_size ? res_size : b_size; - const uint64_t copy_idx = res_size < a_size ? res_size : a_size; - // subtract up to the smallest dimension - for (uint64_t i = 0; i < sub_idx; ++i) { - znx_sub_i64_ref(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then copy to the largest dimension - for (uint64_t i = sub_idx; i < copy_idx; ++i) { - znx_copy_i64_ref(nn, res + i * res_sl, a + i * a_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } - } -} - -EXPORT void vec_znx_rotate_ref(const MODULE* module, // N - const int64_t p, // rotation value - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->nn; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - // rotate up to the smallest dimension - for (uint64_t i = 0; i < rot_end_idx; ++i) { - int64_t* res_ptr = res + i * res_sl; - const int64_t* a_ptr = a + i * a_sl; - if (res_ptr == a_ptr) { - znx_rotate_inplace_i64(nn, p, res_ptr); - } else { - znx_rotate_i64(nn, p, res_ptr, a_ptr); - } - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -} - -EXPORT void vec_znx_mul_xp_minus_one_ref(const MODULE* module, // N - const int64_t p, // p - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->nn; - - const uint64_t rot_end_idx = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < rot_end_idx; ++i) { - int64_t* res_ptr = res + i * res_sl; - const int64_t* a_ptr = a + i * a_sl; - if (res_ptr == a_ptr) { - znx_mul_xp_minus_one_inplace_i64(nn, p, res_ptr); - } else { - znx_mul_xp_minus_one_i64(nn, p, res_ptr, a_ptr); - } - } - // then extend with zeros - for (uint64_t i = rot_end_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -} - -EXPORT void vec_znx_automorphism_ref(const MODULE* module, // N - const int64_t p, // X->X^p - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->nn; - - const uint64_t auto_end_idx = res_size < a_size ? res_size : a_size; - - for (uint64_t i = 0; i < auto_end_idx; ++i) { - int64_t* res_ptr = res + i * res_sl; - const int64_t* a_ptr = a + i * a_sl; - if (res_ptr == a_ptr) { - znx_automorphism_inplace_i64(nn, p, res_ptr); - } else { - znx_automorphism_i64(nn, p, res_ptr, a_ptr); - } - } - // then extend with zeros - for (uint64_t i = auto_end_idx; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -} - -EXPORT void vec_znx_normalize_base2k_ref(const MODULE* module, uint64_t nn, // N - uint64_t log2_base2k, // output base 2^K - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - uint8_t* tmp_space // scratch space of size >= N -) { - - // use MSB limb of res for carry propagation - int64_t* cout = (int64_t*)tmp_space; - int64_t* cin = 0x0; - - // propagate carry until first limb of res - int64_t i = a_size - 1; - for (; i >= res_size; --i) { - znx_normalize(nn, log2_base2k, 0x0, cout, a + i * a_sl, cin); - cin = cout; - } - - // propagate carry and normalize - for (; i >= 1; --i) { - znx_normalize(nn, log2_base2k, res + i * res_sl, cout, a + i * a_sl, cin); - cin = cout; - } - - // normalize last limb - znx_normalize(nn, log2_base2k, res, 0x0, a, cin); - - // extend result with zeros - for (uint64_t i = a_size; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -} - -EXPORT uint64_t vec_znx_normalize_base2k_tmp_bytes_ref(const MODULE* module, uint64_t nn // N -) { - return nn * sizeof(int64_t); -} - -// alias have to be defined in this unit: do not move -#ifdef __APPLE__ -EXPORT uint64_t fft64_vec_znx_big_range_normalize_base2k_tmp_bytes( // - const MODULE* module, // N - uint64_t nn -) { - return vec_znx_normalize_base2k_tmp_bytes_ref(module, nn); -} -EXPORT uint64_t fft64_vec_znx_big_normalize_base2k_tmp_bytes( // - const MODULE* module, // N - uint64_t nn -) { - return vec_znx_normalize_base2k_tmp_bytes_ref(module, nn); -} -#else -EXPORT uint64_t fft64_vec_znx_big_normalize_base2k_tmp_bytes( // - const MODULE* module, // N - uint64_t nn - ) __attribute((alias("vec_znx_normalize_base2k_tmp_bytes_ref"))); - -EXPORT uint64_t fft64_vec_znx_big_range_normalize_base2k_tmp_bytes( // - const MODULE* module, // N - uint64_t nn - ) __attribute((alias("vec_znx_normalize_base2k_tmp_bytes_ref"))); -#endif - -/** @brief sets res = 0 */ -EXPORT void vec_znx_zero(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl // res -) { - module->func.vec_znx_zero(module, res, res_size, res_sl); -} - -/** @brief sets res = a */ -EXPORT void vec_znx_copy(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->func.vec_znx_copy(module, res, res_size, res_sl, a, a_size, a_sl); -} - -/** @brief sets res = a */ -EXPORT void vec_znx_negate(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - module->func.vec_znx_negate(module, res, res_size, res_sl, a, a_size, a_sl); -} - -EXPORT void vec_znx_zero_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl // res -) { - uint64_t nn = module->nn; - for (uint64_t i = 0; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -} - -EXPORT void vec_znx_copy_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - uint64_t nn = module->nn; - uint64_t smin = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < smin; ++i) { - znx_copy_i64_ref(nn, res + i * res_sl, a + i * a_sl); - } - for (uint64_t i = smin; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -} - -EXPORT void vec_znx_negate_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - uint64_t nn = module->nn; - uint64_t smin = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < smin; ++i) { - znx_negate_i64_ref(nn, res + i * res_sl, a + i * a_sl); - } - for (uint64_t i = smin; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -}
@@ -1,370 +0,0 @@
-#ifndef SPQLIOS_VEC_ZNX_ARITHMETIC_H -#define SPQLIOS_VEC_ZNX_ARITHMETIC_H - -#include <stdint.h> - -#include "../commons.h" -#include "../reim/reim_fft.h" - -/** - * We support the following module families: - * - FFT64: - * all the polynomials should fit at all times over 52 bits. - * for FHE implementations, the recommended limb-sizes are - * between K=10 and 20, which is good for low multiplicative depths. - * - NTT120: - * all the polynomials should fit at all times over 119 bits. - * for FHE implementations, the recommended limb-sizes are - * between K=20 and 40, which is good for large multiplicative depths. - */ -typedef enum module_type_t { FFT64, NTT120 } MODULE_TYPE; - -/** @brief opaque structure that describr the modules (ZnX,TnX) and the hardware */ -typedef struct module_info_t MODULE; -/** @brief opaque type that represents a prepared matrix */ -typedef struct vmp_pmat_t VMP_PMAT; -/** @brief opaque type that represents a vector of znx in DFT space */ -typedef struct vec_znx_dft_t VEC_ZNX_DFT; -/** @brief opaque type that represents a vector of znx in large coeffs space */ -typedef struct vec_znx_bigcoeff_t VEC_ZNX_BIG; -/** @brief opaque type that represents a prepared scalar vector product */ -typedef struct svp_ppol_t SVP_PPOL; -/** @brief opaque type that represents a prepared left convolution vector product */ -typedef struct cnv_pvec_l_t CNV_PVEC_L; -/** @brief opaque type that represents a prepared right convolution vector product */ -typedef struct cnv_pvec_r_t CNV_PVEC_R; - -/** @brief bytes needed for a vec_znx in DFT space */ -EXPORT uint64_t bytes_of_vec_znx_dft(const MODULE* module, // N - uint64_t size); - -/** @brief allocates a vec_znx in DFT space */ -EXPORT VEC_ZNX_DFT* new_vec_znx_dft(const MODULE* module, // N - uint64_t size); - -/** @brief frees memory from a vec_znx in DFT space */ -EXPORT void delete_vec_znx_dft(VEC_ZNX_DFT* res); - -/** @brief bytes needed for a vec_znx_big */ -EXPORT uint64_t bytes_of_vec_znx_big(const MODULE* module, // N - uint64_t size); - -/** @brief allocates a vec_znx_big */ -EXPORT VEC_ZNX_BIG* new_vec_znx_big(const MODULE* module, // N - uint64_t size); -/** @brief frees memory from a vec_znx_big */ -EXPORT void delete_vec_znx_big(VEC_ZNX_BIG* res); - -/** @brief bytes needed for a prepared vector */ -EXPORT uint64_t bytes_of_svp_ppol(const MODULE* module); // N - -/** @brief allocates a prepared vector */ -EXPORT SVP_PPOL* new_svp_ppol(const MODULE* module); // N - -/** @brief frees memory for a prepared vector */ -EXPORT void delete_svp_ppol(SVP_PPOL* res); - -/** @brief bytes needed for a prepared matrix */ -EXPORT uint64_t bytes_of_vmp_pmat(const MODULE* module, // N - uint64_t nrows, uint64_t ncols); - -/** @brief allocates a prepared matrix */ -EXPORT VMP_PMAT* new_vmp_pmat(const MODULE* module, // N - uint64_t nrows, uint64_t ncols); - -/** @brief frees memory for a prepared matrix */ -EXPORT void delete_vmp_pmat(VMP_PMAT* res); - -/** - * @brief obtain a module info for ring dimension N - * the module-info knows about: - * - the dimension N (or the complex dimension m=N/2) - * - any moduleuted fft or ntt items - * - the hardware (avx, arm64, x86, ...) - */ -EXPORT MODULE* new_module_info(uint64_t N, MODULE_TYPE mode); -EXPORT void delete_module_info(MODULE* module_info); -EXPORT uint64_t module_get_n(const MODULE* module); - -/** @brief sets res = 0 */ -EXPORT void vec_znx_zero(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl // res -); - -/** @brief sets res = a */ -EXPORT void vec_znx_copy(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a */ -EXPORT void vec_znx_negate(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a + b */ -EXPORT void vec_znx_add(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a - b */ -EXPORT void vec_znx_sub(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = k-normalize-reduce(a) */ -EXPORT void vec_znx_normalize_base2k(const MODULE* module, // MODULE - uint64_t nn, // N - uint64_t log2_base2k, // output base 2^K - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - uint8_t* tmp_space // scratch space (size >= N) -); - -/** @brief returns the minimal byte length of scratch space for vec_znx_normalize_base2k */ -EXPORT uint64_t vec_znx_normalize_base2k_tmp_bytes(const MODULE* module, uint64_t nn // N -); - -/** @brief sets res = a . X^p */ -EXPORT void vec_znx_rotate(const MODULE* module, // N - const int64_t p, // rotation value - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a * (X^{p} - 1) */ -EXPORT void vec_znx_mul_xp_minus_one(const MODULE* module, // N - const int64_t p, // rotation value - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = a(X^p) */ -EXPORT void vec_znx_automorphism(const MODULE* module, // N - const int64_t p, // X-X^p - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = 0 */ -EXPORT void vec_dft_zero(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size // res -); - -/** @brief sets res = a+b */ -EXPORT void vec_dft_add(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -); - -/** @brief sets res = a-b */ -EXPORT void vec_dft_sub(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -); - -/** @brief sets res = DFT(a) */ -EXPORT void vec_znx_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief sets res = iDFT(a_dft) -- output in big coeffs space */ -EXPORT void vec_znx_idft(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - uint8_t* tmp // scratch space -); - -/** @brief tmp bytes required for vec_znx_idft */ -EXPORT uint64_t vec_znx_idft_tmp_bytes(const MODULE* module, uint64_t nn); - -/** - * @brief sets res = iDFT(a_dft) -- output in big coeffs space - * - * @note a_dft is overwritten - */ -EXPORT void vec_znx_idft_tmp_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - VEC_ZNX_DFT* a_dft, uint64_t a_size // a is overwritten -); - -/** @brief sets res = a+b */ -EXPORT void vec_znx_big_add(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -); -/** @brief sets res = a+b */ -EXPORT void vec_znx_big_add_small(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); -EXPORT void vec_znx_big_add_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a-b */ -EXPORT void vec_znx_big_sub(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -); -EXPORT void vec_znx_big_sub_small_b(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); -EXPORT void vec_znx_big_sub_small_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -); -EXPORT void vec_znx_big_sub_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = k-normalize(a) -- output in int64 coeffs space */ -EXPORT void vec_znx_big_normalize_base2k(const MODULE* module, // MODULE - uint64_t nn, // N - uint64_t log2_base2k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - uint8_t* tmp_space // temp space -); - -/** @brief returns the minimal byte length of scratch space for vec_znx_big_normalize_base2k */ -EXPORT uint64_t vec_znx_big_normalize_base2k_tmp_bytes(const MODULE* module, uint64_t nn // N -); - -/** @brief sets res = k-normalize(a.subrange) -- output in int64 coeffs space */ -EXPORT void vec_znx_big_range_normalize_base2k( // - const MODULE* module, // MODULE - uint64_t nn, - uint64_t log2_base2k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_range_begin, uint64_t a_range_xend, uint64_t a_range_step, // range - uint8_t* tmp_space // temp space -); - -/** @brief returns the minimal byte length of scratch space for vec_znx_big_range_normalize_base2k */ -EXPORT uint64_t vec_znx_big_range_normalize_base2k_tmp_bytes( // - const MODULE* module, uint64_t nn // N -); - -/** @brief sets res = a . X^p */ -EXPORT void vec_znx_big_rotate(const MODULE* module, // N - int64_t p, // rotation value - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -); - -/** @brief sets res = a(X^p) */ -EXPORT void vec_znx_big_automorphism(const MODULE* module, // N - int64_t p, // X-X^p - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -); - -/** @brief apply a svp product, result = ppol * a, presented in DFT space */ -EXPORT void svp_apply_dft(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, // output - const SVP_PPOL* ppol, // prepared pol - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief apply a svp product, result = ppol * a, presented in DFT space */ -EXPORT void svp_apply_dft_to_dft(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, - uint64_t res_cols, // output - const SVP_PPOL* ppol, // prepared pol - const VEC_ZNX_DFT* a, uint64_t a_size, uint64_t a_cols // a -); - -/** @brief prepares a svp polynomial */ -EXPORT void svp_prepare(const MODULE* module, // N - SVP_PPOL* ppol, // output - const int64_t* pol // a -); - -/** @brief res = a * b : small integer polynomial product */ -EXPORT void znx_small_single_product(const MODULE* module, // N - int64_t* res, // output - const int64_t* a, // a - const int64_t* b, // b - uint8_t* tmp); - -/** @brief tmp bytes required for znx_small_single_product */ -EXPORT uint64_t znx_small_single_product_tmp_bytes(const MODULE* module, uint64_t nn); - -/** @brief minimal scratch space byte-size required for the vmp_prepare function */ -EXPORT uint64_t vmp_prepare_tmp_bytes(const MODULE* module, uint64_t nn, // N - uint64_t nrows, uint64_t ncols); - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void vmp_prepare_contiguous(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); - -/** @brief applies a vmp product (result in DFT space) adds to res inplace */ -EXPORT void vmp_apply_dft_add(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, uint64_t pmat_scale, // prep matrix - uint8_t* tmp_space // scratch space -); - -/** @brief applies a vmp product (result in DFT space) */ -EXPORT void vmp_apply_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -); - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t vmp_apply_dft_tmp_bytes(const MODULE* module, uint64_t nn, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); - -/** @brief applies vmp product */ -EXPORT void vmp_apply_dft_to_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, - const uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); - -/** @brief applies vmp product and adds to res inplace */ -EXPORT void vmp_apply_dft_to_dft_add(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, const uint64_t ncols, - const uint64_t pmat_scale, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t vmp_apply_dft_to_dft_tmp_bytes(const MODULE* module, uint64_t nn, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); -#endif // SPQLIOS_VEC_ZNX_ARITHMETIC_H
@@ -1,563 +0,0 @@
-#ifndef SPQLIOS_VEC_ZNX_ARITHMETIC_PRIVATE_H -#define SPQLIOS_VEC_ZNX_ARITHMETIC_PRIVATE_H - -#include "../commons_private.h" -#include "../q120/q120_ntt.h" -#include "vec_znx_arithmetic.h" - -/** - * Layouts families: - * - * fft64: - * K: <= 20, N: <= 65536, ell: <= 200 - * vec<ZnX> normalized: represented by int64 - * vec<ZnX> large: represented by int64 (expect <=52 bits) - * vec<ZnX> DFT: represented by double (reim_fft space) - * On AVX2 inftastructure, PMAT, LCNV, RCNV use a special reim4_fft space - * - * ntt120: - * K: <= 50, N: <= 65536, ell: <= 80 - * vec<ZnX> normalized: represented by int64 - * vec<ZnX> large: represented by int128 (expect <=120 bits) - * vec<ZnX> DFT: represented by int64x4 (ntt120 space) - * On AVX2 inftastructure, PMAT, LCNV, RCNV use a special ntt120 space - * - * ntt104: - * K: <= 40, N: <= 65536, ell: <= 80 - * vec<ZnX> normalized: represented by int64 - * vec<ZnX> large: represented by int128 (expect <=120 bits) - * vec<ZnX> DFT: represented by int64x4 (ntt120 space) - * On AVX512 inftastructure, PMAT, LCNV, RCNV use a special ntt104 space - */ - -struct fft64_module_info_t { - // pre-computation for reim_fft - REIM_FFT_PRECOMP* p_fft; - // pre-computation for add_fft - REIM_FFTVEC_ADD_PRECOMP* add_fft; - // pre-computation for add_fft - REIM_FFTVEC_SUB_PRECOMP* sub_fft; - // pre-computation for mul_fft - REIM_FFTVEC_MUL_PRECOMP* mul_fft; - // pre-computation for reim_from_znx6 - REIM_FROM_ZNX64_PRECOMP* p_conv; - // pre-computation for reim_tp_znx6 - REIM_TO_ZNX64_PRECOMP* p_reim_to_znx; - // pre-computation for reim_fft - REIM_IFFT_PRECOMP* p_ifft; - // pre-computation for reim_fftvec_addmul - REIM_FFTVEC_ADDMUL_PRECOMP* p_addmul; -}; - -struct q120_module_info_t { - // pre-computation for q120b to q120b ntt - q120_ntt_precomp* p_ntt; - // pre-computation for q120b to q120b intt - q120_ntt_precomp* p_intt; -}; - -// TODO add function types here -typedef typeof(vec_znx_zero) VEC_ZNX_ZERO_F; -typedef typeof(vec_znx_copy) VEC_ZNX_COPY_F; -typedef typeof(vec_znx_negate) VEC_ZNX_NEGATE_F; -typedef typeof(vec_znx_add) VEC_ZNX_ADD_F; -typedef typeof(vec_znx_dft) VEC_ZNX_DFT_F; -typedef typeof(vec_dft_add) VEC_DFT_ADD_F; -typedef typeof(vec_dft_sub) VEC_DFT_SUB_F; -typedef typeof(vec_znx_idft) VEC_ZNX_IDFT_F; -typedef typeof(vec_znx_idft_tmp_bytes) VEC_ZNX_IDFT_TMP_BYTES_F; -typedef typeof(vec_znx_idft_tmp_a) VEC_ZNX_IDFT_TMP_A_F; -typedef typeof(vec_znx_sub) VEC_ZNX_SUB_F; -typedef typeof(vec_znx_rotate) VEC_ZNX_ROTATE_F; -typedef typeof(vec_znx_mul_xp_minus_one) VEC_ZNX_MUL_XP_MINUS_ONE_F; -typedef typeof(vec_znx_automorphism) VEC_ZNX_AUTOMORPHISM_F; -typedef typeof(vec_znx_normalize_base2k) VEC_ZNX_NORMALIZE_BASE2K_F; -typedef typeof(vec_znx_normalize_base2k_tmp_bytes) VEC_ZNX_NORMALIZE_BASE2K_TMP_BYTES_F; -typedef typeof(vec_znx_big_normalize_base2k) VEC_ZNX_BIG_NORMALIZE_BASE2K_F; -typedef typeof(vec_znx_big_normalize_base2k_tmp_bytes) VEC_ZNX_BIG_NORMALIZE_BASE2K_TMP_BYTES_F; -typedef typeof(vec_znx_big_range_normalize_base2k) VEC_ZNX_BIG_RANGE_NORMALIZE_BASE2K_F; -typedef typeof(vec_znx_big_range_normalize_base2k_tmp_bytes) VEC_ZNX_BIG_RANGE_NORMALIZE_BASE2K_TMP_BYTES_F; -typedef typeof(vec_znx_big_add) VEC_ZNX_BIG_ADD_F; -typedef typeof(vec_znx_big_add_small) VEC_ZNX_BIG_ADD_SMALL_F; -typedef typeof(vec_znx_big_add_small2) VEC_ZNX_BIG_ADD_SMALL2_F; -typedef typeof(vec_znx_big_sub) VEC_ZNX_BIG_SUB_F; -typedef typeof(vec_znx_big_sub_small_a) VEC_ZNX_BIG_SUB_SMALL_A_F; -typedef typeof(vec_znx_big_sub_small_b) VEC_ZNX_BIG_SUB_SMALL_B_F; -typedef typeof(vec_znx_big_sub_small2) VEC_ZNX_BIG_SUB_SMALL2_F; -typedef typeof(vec_znx_big_rotate) VEC_ZNX_BIG_ROTATE_F; -typedef typeof(vec_znx_big_automorphism) VEC_ZNX_BIG_AUTOMORPHISM_F; -typedef typeof(svp_prepare) SVP_PREPARE; -typedef typeof(svp_apply_dft) SVP_APPLY_DFT_F; -typedef typeof(svp_apply_dft_to_dft) SVP_APPLY_DFT_TO_DFT_F; -typedef typeof(znx_small_single_product) ZNX_SMALL_SINGLE_PRODUCT_F; -typedef typeof(znx_small_single_product_tmp_bytes) ZNX_SMALL_SINGLE_PRODUCT_TMP_BYTES_F; -typedef typeof(vmp_prepare_contiguous) VMP_PREPARE_CONTIGUOUS_F; -typedef typeof(vmp_prepare_tmp_bytes) VMP_PREPARE_TMP_BYTES_F; -typedef typeof(vmp_apply_dft) VMP_APPLY_DFT_F; -typedef typeof(vmp_apply_dft_add) VMP_APPLY_DFT_ADD_F; -typedef typeof(vmp_apply_dft_tmp_bytes) VMP_APPLY_DFT_TMP_BYTES_F; -typedef typeof(vmp_apply_dft_to_dft) VMP_APPLY_DFT_TO_DFT_F; -typedef typeof(vmp_apply_dft_to_dft_add) VMP_APPLY_DFT_TO_DFT_ADD_F; -typedef typeof(vmp_apply_dft_to_dft_tmp_bytes) VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F; -typedef typeof(bytes_of_vec_znx_dft) BYTES_OF_VEC_ZNX_DFT_F; -typedef typeof(bytes_of_vec_znx_big) BYTES_OF_VEC_ZNX_BIG_F; -typedef typeof(bytes_of_svp_ppol) BYTES_OF_SVP_PPOL_F; -typedef typeof(bytes_of_vmp_pmat) BYTES_OF_VMP_PMAT_F; - -struct module_virtual_functions_t { - // TODO add functions here - VEC_ZNX_ZERO_F* vec_znx_zero; - VEC_ZNX_COPY_F* vec_znx_copy; - VEC_ZNX_NEGATE_F* vec_znx_negate; - VEC_ZNX_ADD_F* vec_znx_add; - VEC_ZNX_DFT_F* vec_znx_dft; - VEC_DFT_ADD_F* vec_dft_add; - VEC_DFT_SUB_F* vec_dft_sub; - VEC_ZNX_IDFT_F* vec_znx_idft; - VEC_ZNX_IDFT_TMP_BYTES_F* vec_znx_idft_tmp_bytes; - VEC_ZNX_IDFT_TMP_A_F* vec_znx_idft_tmp_a; - VEC_ZNX_SUB_F* vec_znx_sub; - VEC_ZNX_ROTATE_F* vec_znx_rotate; - VEC_ZNX_MUL_XP_MINUS_ONE_F* vec_znx_mul_xp_minus_one; - VEC_ZNX_AUTOMORPHISM_F* vec_znx_automorphism; - VEC_ZNX_NORMALIZE_BASE2K_F* vec_znx_normalize_base2k; - VEC_ZNX_NORMALIZE_BASE2K_TMP_BYTES_F* vec_znx_normalize_base2k_tmp_bytes; - VEC_ZNX_BIG_NORMALIZE_BASE2K_F* vec_znx_big_normalize_base2k; - VEC_ZNX_BIG_NORMALIZE_BASE2K_TMP_BYTES_F* vec_znx_big_normalize_base2k_tmp_bytes; - VEC_ZNX_BIG_RANGE_NORMALIZE_BASE2K_F* vec_znx_big_range_normalize_base2k; - VEC_ZNX_BIG_RANGE_NORMALIZE_BASE2K_TMP_BYTES_F* vec_znx_big_range_normalize_base2k_tmp_bytes; - VEC_ZNX_BIG_ADD_F* vec_znx_big_add; - VEC_ZNX_BIG_ADD_SMALL_F* vec_znx_big_add_small; - VEC_ZNX_BIG_ADD_SMALL2_F* vec_znx_big_add_small2; - VEC_ZNX_BIG_SUB_F* vec_znx_big_sub; - VEC_ZNX_BIG_SUB_SMALL_A_F* vec_znx_big_sub_small_a; - VEC_ZNX_BIG_SUB_SMALL_B_F* vec_znx_big_sub_small_b; - VEC_ZNX_BIG_SUB_SMALL2_F* vec_znx_big_sub_small2; - VEC_ZNX_BIG_ROTATE_F* vec_znx_big_rotate; - VEC_ZNX_BIG_AUTOMORPHISM_F* vec_znx_big_automorphism; - SVP_PREPARE* svp_prepare; - SVP_APPLY_DFT_F* svp_apply_dft; - SVP_APPLY_DFT_TO_DFT_F* svp_apply_dft_to_dft; - ZNX_SMALL_SINGLE_PRODUCT_F* znx_small_single_product; - ZNX_SMALL_SINGLE_PRODUCT_TMP_BYTES_F* znx_small_single_product_tmp_bytes; - VMP_PREPARE_CONTIGUOUS_F* vmp_prepare_contiguous; - VMP_PREPARE_TMP_BYTES_F* vmp_prepare_tmp_bytes; - VMP_APPLY_DFT_F* vmp_apply_dft; - VMP_APPLY_DFT_ADD_F* vmp_apply_dft_add; - VMP_APPLY_DFT_TMP_BYTES_F* vmp_apply_dft_tmp_bytes; - VMP_APPLY_DFT_TO_DFT_F* vmp_apply_dft_to_dft; - VMP_APPLY_DFT_TO_DFT_ADD_F* vmp_apply_dft_to_dft_add; - VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* vmp_apply_dft_to_dft_tmp_bytes; - BYTES_OF_VEC_ZNX_DFT_F* bytes_of_vec_znx_dft; - BYTES_OF_VEC_ZNX_BIG_F* bytes_of_vec_znx_big; - BYTES_OF_SVP_PPOL_F* bytes_of_svp_ppol; - BYTES_OF_VMP_PMAT_F* bytes_of_vmp_pmat; -}; - -union backend_module_info_t { - struct fft64_module_info_t fft64; - struct q120_module_info_t q120; -}; - -struct module_info_t { - // generic parameters - MODULE_TYPE module_type; - uint64_t nn; - uint64_t m; - // backend_dependent functions - union backend_module_info_t mod; - // virtual functions - struct module_virtual_functions_t func; -}; - -EXPORT uint64_t fft64_bytes_of_vec_znx_dft(const MODULE* module, // N - uint64_t size); - -EXPORT uint64_t fft64_bytes_of_vec_znx_big(const MODULE* module, // N - uint64_t size); - -EXPORT uint64_t fft64_bytes_of_svp_ppol(const MODULE* module); // N - -EXPORT uint64_t fft64_bytes_of_vmp_pmat(const MODULE* module, // N - uint64_t nrows, uint64_t ncols); - -EXPORT void vec_znx_zero_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl // res -); - -EXPORT void vec_znx_copy_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_znx_negate_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_znx_negate_avx(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_znx_add_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); -EXPORT void vec_znx_add_avx(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -EXPORT void vec_znx_sub_ref(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -EXPORT void vec_znx_sub_avx(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -EXPORT void vec_znx_normalize_base2k_ref(const MODULE* module, uint64_t nn, // N - uint64_t log2_base2k, // output base 2^K - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // inp - uint8_t* tmp_space // scratch space -); - -EXPORT uint64_t vec_znx_normalize_base2k_tmp_bytes_ref(const MODULE* module, uint64_t nn // N -); - -EXPORT void vec_znx_rotate_ref(const MODULE* module, // N - const int64_t p, // rotation value - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_znx_mul_xp_minus_one_ref(const MODULE* module, // N - const int64_t p, // rotation value - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_znx_automorphism_ref(const MODULE* module, // N - const int64_t p, // X->X^p - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vmp_prepare_ref(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols // a -); - -EXPORT void vmp_apply_dft_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols // prep matrix -); - -EXPORT void vec_dft_zero_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size // res -); - -EXPORT void vec_dft_add_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -); - -EXPORT void vec_dft_sub_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -); - -EXPORT void vec_dft_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void vec_idft_ref(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size); - -EXPORT void vec_znx_big_normalize_ref(const MODULE* module, // MODULE - uint64_t nn, // N - uint64_t k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -); - -/** @brief apply a svp product, result = ppol * a, presented in DFT space */ -EXPORT void fft64_svp_apply_dft_ref(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, // output - const SVP_PPOL* ppol, // prepared pol - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** @brief apply a svp product, result = ppol * a, presented in DFT space */ -EXPORT void fft64_svp_apply_dft_to_dft_ref(const MODULE* module, // N - const VEC_ZNX_DFT* res, uint64_t res_size, - uint64_t res_cols, // output - const SVP_PPOL* ppol, // prepared pol - const VEC_ZNX_DFT* a, uint64_t a_size, - uint64_t a_cols // a -); - -/** @brief sets res = k-normalize(a) -- output in int64 coeffs space */ -EXPORT void fft64_vec_znx_big_normalize_base2k(const MODULE* module, // MODULE - uint64_t nn, // N - uint64_t k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - uint8_t* tmp_space // temp space -); - -/** @brief returns the minimal byte length of scratch space for vec_znx_big_normalize_base2k */ -EXPORT uint64_t fft64_vec_znx_big_normalize_base2k_tmp_bytes(const MODULE* module, uint64_t nn // N - -); - -/** @brief sets res = k-normalize(a.subrange) -- output in int64 coeffs space */ -EXPORT void fft64_vec_znx_big_range_normalize_base2k(const MODULE* module, // MODULE - uint64_t nn, - uint64_t log2_base2k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_range_begin, // a - uint64_t a_range_xend, uint64_t a_range_step, // range - uint8_t* tmp_space // temp space -); - -/** @brief returns the minimal byte length of scratch space for vec_znx_big_range_normalize_base2k */ -EXPORT uint64_t fft64_vec_znx_big_range_normalize_base2k_tmp_bytes(const MODULE* module, uint64_t nn // N -); - -EXPORT void fft64_vec_znx_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -EXPORT void fft64_vec_dft_add(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -); - -EXPORT void fft64_vec_dft_sub(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -); - -EXPORT void fft64_vec_znx_idft(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - uint8_t* tmp // scratch space -); - -EXPORT uint64_t fft64_vec_znx_idft_tmp_bytes(const MODULE* module, uint64_t nn); - -EXPORT void fft64_vec_znx_idft_tmp_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - VEC_ZNX_DFT* a_dft, uint64_t a_size // a is overwritten -); - -EXPORT void ntt120_vec_znx_dft_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -); - -/** */ -EXPORT void ntt120_vec_znx_idft_avx(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - uint8_t* tmp // scratch space -); - -EXPORT uint64_t ntt120_vec_znx_idft_tmp_bytes_avx(const MODULE* module, uint64_t nn); - -EXPORT void ntt120_vec_znx_idft_tmp_a_avx(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - VEC_ZNX_DFT* a_dft, uint64_t a_size // a is overwritten -); - -// big additions/subtractions - -/** @brief sets res = a+b */ -EXPORT void fft64_vec_znx_big_add(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -); -/** @brief sets res = a+b */ -EXPORT void fft64_vec_znx_big_add_small(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); -EXPORT void fft64_vec_znx_big_add_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a-b */ -EXPORT void fft64_vec_znx_big_sub(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -); -EXPORT void fft64_vec_znx_big_sub_small_b(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); -EXPORT void fft64_vec_znx_big_sub_small_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -); -EXPORT void fft64_vec_znx_big_sub_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -); - -/** @brief sets res = a . X^p */ -EXPORT void fft64_vec_znx_big_rotate(const MODULE* module, // N - int64_t p, // rotation value - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -); - -/** @brief sets res = a(X^p) */ -EXPORT void fft64_vec_znx_big_automorphism(const MODULE* module, // N - int64_t p, // X-X^p - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -); - -/** @brief prepares a svp polynomial */ -EXPORT void fft64_svp_prepare_ref(const MODULE* module, // N - SVP_PPOL* ppol, // output - const int64_t* pol // a -); - -/** @brief res = a * b : small integer polynomial product */ -EXPORT void fft64_znx_small_single_product(const MODULE* module, // N - int64_t* res, // output - const int64_t* a, // a - const int64_t* b, // b - uint8_t* tmp); - -/** @brief tmp bytes required for znx_small_single_product */ -EXPORT uint64_t fft64_znx_small_single_product_tmp_bytes(const MODULE* module, uint64_t nn); - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void fft64_vmp_prepare_contiguous_ref(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void fft64_vmp_prepare_contiguous_avx(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -); - -/** @brief minimal scratch space byte-size required for the vmp_prepare function */ -EXPORT uint64_t fft64_vmp_prepare_tmp_bytes(const MODULE* module, uint64_t nn, // N - uint64_t nrows, uint64_t ncols); - -/** @brief applies a vmp product (result in DFT space) and adds to res inplace */ -EXPORT void fft64_vmp_apply_dft_add_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, - uint64_t pmat_scale, // prep matrix - uint8_t* tmp_space // scratch space -); - -/** @brief applies a vmp product (result in DFT space) */ -EXPORT void fft64_vmp_apply_dft_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -); - -/** @brief applies a vmp product (result in DFT space) */ -EXPORT void fft64_vmp_apply_dft_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -); - -/** @brief applies a vmp product (result in DFT space) and adds to res inplace*/ -EXPORT void fft64_vmp_apply_dft_add_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, - uint64_t pmat_scale, // prep matrix - uint8_t* tmp_space // scratch space -); - -/** @brief this inner function could be very handy */ -EXPORT void fft64_vmp_apply_dft_to_dft_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, - const uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); - -/** @brief applies rmp product and adds to res inplace */ -EXPORT void fft64_vmp_apply_dft_to_dft_add_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, const uint64_t ncols, - uint64_t pmat_scale, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); - -/** @brief this inner function could be very handy */ -EXPORT void fft64_vmp_apply_dft_to_dft_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, - const uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); - -/** @brief applies rmp product and adds to res inplace */ -EXPORT void fft64_vmp_apply_dft_to_dft_add_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, const uint64_t ncols, - uint64_t pmat_scale, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -); - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t fft64_vmp_apply_dft_tmp_bytes(const MODULE* module, uint64_t nn, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t fft64_vmp_apply_dft_to_dft_tmp_bytes(const MODULE* module, uint64_t nn, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -); -#endif // SPQLIOS_VEC_ZNX_ARITHMETIC_PRIVATE_H
@@ -1,103 +0,0 @@
-#include <string.h> - -#include "../coeffs/coeffs_arithmetic.h" -#include "../reim4/reim4_arithmetic.h" -#include "vec_znx_arithmetic_private.h" - -// specialized function (ref) - -// Note: these functions do not have an avx variant. -#define znx_copy_i64_avx znx_copy_i64_ref -#define znx_zero_i64_avx znx_zero_i64_ref - -EXPORT void vec_znx_add_avx(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->nn; - if (a_size <= b_size) { - const uint64_t sum_idx = res_size < a_size ? res_size : a_size; - const uint64_t copy_idx = res_size < b_size ? res_size : b_size; - // add up to the smallest dimension - for (uint64_t i = 0; i < sum_idx; ++i) { - znx_add_i64_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then copy to the largest dimension - for (uint64_t i = sum_idx; i < copy_idx; ++i) { - znx_copy_i64_avx(nn, res + i * res_sl, b + i * b_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_avx(nn, res + i * res_sl); - } - } else { - const uint64_t sum_idx = res_size < b_size ? res_size : b_size; - const uint64_t copy_idx = res_size < a_size ? res_size : a_size; - // add up to the smallest dimension - for (uint64_t i = 0; i < sum_idx; ++i) { - znx_add_i64_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then copy to the largest dimension - for (uint64_t i = sum_idx; i < copy_idx; ++i) { - znx_copy_i64_avx(nn, res + i * res_sl, a + i * a_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_avx(nn, res + i * res_sl); - } - } -} - -EXPORT void vec_znx_sub_avx(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t nn = module->nn; - if (a_size <= b_size) { - const uint64_t sub_idx = res_size < a_size ? res_size : a_size; - const uint64_t copy_idx = res_size < b_size ? res_size : b_size; - // subtract up to the smallest dimension - for (uint64_t i = 0; i < sub_idx; ++i) { - znx_sub_i64_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then negate to the largest dimension - for (uint64_t i = sub_idx; i < copy_idx; ++i) { - znx_negate_i64_avx(nn, res + i * res_sl, b + i * b_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_avx(nn, res + i * res_sl); - } - } else { - const uint64_t sub_idx = res_size < b_size ? res_size : b_size; - const uint64_t copy_idx = res_size < a_size ? res_size : a_size; - // subtract up to the smallest dimension - for (uint64_t i = 0; i < sub_idx; ++i) { - znx_sub_i64_avx(nn, res + i * res_sl, a + i * a_sl, b + i * b_sl); - } - // then copy to the largest dimension - for (uint64_t i = sub_idx; i < copy_idx; ++i) { - znx_copy_i64_avx(nn, res + i * res_sl, a + i * a_sl); - } - // then extend with zeros - for (uint64_t i = copy_idx; i < res_size; ++i) { - znx_zero_i64_avx(nn, res + i * res_sl); - } - } -} - -EXPORT void vec_znx_negate_avx(const MODULE* module, // N - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - uint64_t nn = module->nn; - uint64_t smin = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < smin; ++i) { - znx_negate_i64_avx(nn, res + i * res_sl, a + i * a_sl); - } - for (uint64_t i = smin; i < res_size; ++i) { - znx_zero_i64_ref(nn, res + i * res_sl); - } -}
@@ -1,278 +0,0 @@
-#include "vec_znx_arithmetic_private.h" - -EXPORT uint64_t bytes_of_vec_znx_big(const MODULE* module, // N - uint64_t size) { - return module->func.bytes_of_vec_znx_big(module, size); -} - -// public wrappers - -/** @brief sets res = a+b */ -EXPORT void vec_znx_big_add(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -) { - module->func.vec_znx_big_add(module, res, res_size, a, a_size, b, b_size); -} - -/** @brief sets res = a+b */ -EXPORT void vec_znx_big_add_small(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - module->func.vec_znx_big_add_small(module, res, res_size, a, a_size, b, b_size, b_sl); -} - -EXPORT void vec_znx_big_add_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - module->func.vec_znx_big_add_small2(module, res, res_size, a, a_size, a_sl, b, b_size, b_sl); -} - -/** @brief sets res = a-b */ -EXPORT void vec_znx_big_sub(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -) { - module->func.vec_znx_big_sub(module, res, res_size, a, a_size, b, b_size); -} - -EXPORT void vec_znx_big_sub_small_b(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - module->func.vec_znx_big_sub_small_b(module, res, res_size, a, a_size, b, b_size, b_sl); -} - -EXPORT void vec_znx_big_sub_small_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -) { - module->func.vec_znx_big_sub_small_a(module, res, res_size, a, a_size, a_sl, b, b_size); -} -EXPORT void vec_znx_big_sub_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - module->func.vec_znx_big_sub_small2(module, res, res_size, a, a_size, a_sl, b, b_size, b_sl); -} - -/** @brief sets res = a . X^p */ -EXPORT void vec_znx_big_rotate(const MODULE* module, // N - int64_t p, // rotation value - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -) { - module->func.vec_znx_big_rotate(module, p, res, res_size, a, a_size); -} - -/** @brief sets res = a(X^p) */ -EXPORT void vec_znx_big_automorphism(const MODULE* module, // N - int64_t p, // X-X^p - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -) { - module->func.vec_znx_big_automorphism(module, p, res, res_size, a, a_size); -} - -// private wrappers - -EXPORT uint64_t fft64_bytes_of_vec_znx_big(const MODULE* module, // N - uint64_t size) { - return module->nn * size * sizeof(double); -} - -EXPORT VEC_ZNX_BIG* new_vec_znx_big(const MODULE* module, // N - uint64_t size) { - return spqlios_alloc(bytes_of_vec_znx_big(module, size)); -} - -EXPORT void delete_vec_znx_big(VEC_ZNX_BIG* res) { spqlios_free(res); } - -/** @brief sets res = a+b */ -EXPORT void fft64_vec_znx_big_add(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -) { - const uint64_t n = module->nn; - vec_znx_add(module, // - (int64_t*)res, res_size, n, // - (int64_t*)a, a_size, n, // - (int64_t*)b, b_size, n); -} -/** @brief sets res = a+b */ -EXPORT void fft64_vec_znx_big_add_small(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t n = module->nn; - vec_znx_add(module, // - (int64_t*)res, res_size, n, // - (int64_t*)a, a_size, n, // - b, b_size, b_sl); -} -EXPORT void fft64_vec_znx_big_add_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t n = module->nn; - vec_znx_add(module, // - (int64_t*)res, res_size, n, // - a, a_size, a_sl, // - b, b_size, b_sl); -} - -/** @brief sets res = a-b */ -EXPORT void fft64_vec_znx_big_sub(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -) { - const uint64_t n = module->nn; - vec_znx_sub(module, // - (int64_t*)res, res_size, n, // - (int64_t*)a, a_size, n, // - (int64_t*)b, b_size, n); -} - -EXPORT void fft64_vec_znx_big_sub_small_b(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t n = module->nn; - vec_znx_sub(module, // - (int64_t*)res, res_size, n, // - (int64_t*)a, a_size, // - n, b, b_size, b_sl); -} -EXPORT void fft64_vec_znx_big_sub_small_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VEC_ZNX_BIG* b, uint64_t b_size // b -) { - const uint64_t n = module->nn; - vec_znx_sub(module, // - (int64_t*)res, res_size, n, // - a, a_size, a_sl, // - (int64_t*)b, b_size, n); -} -EXPORT void fft64_vec_znx_big_sub_small2(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const int64_t* b, uint64_t b_size, uint64_t b_sl // b -) { - const uint64_t n = module->nn; - vec_znx_sub(module, // - (int64_t*)res, res_size, // - n, a, a_size, // - a_sl, b, b_size, b_sl); -} - -/** @brief sets res = a . X^p */ -EXPORT void fft64_vec_znx_big_rotate(const MODULE* module, // N - int64_t p, // rotation value - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -) { - uint64_t nn = module->nn; - vec_znx_rotate(module, p, (int64_t*)res, res_size, nn, (int64_t*)a, a_size, nn); -} - -/** @brief sets res = a(X^p) */ -EXPORT void fft64_vec_znx_big_automorphism(const MODULE* module, // N - int64_t p, // X-X^p - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_BIG* a, uint64_t a_size // a -) { - uint64_t nn = module->nn; - vec_znx_automorphism(module, p, (int64_t*)res, res_size, nn, (int64_t*)a, a_size, nn); -} - -EXPORT void vec_znx_big_normalize_base2k(const MODULE* module, // MODULE - uint64_t nn, // N - uint64_t k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - uint8_t* tmp_space // temp space -) { - module->func.vec_znx_big_normalize_base2k(module, // MODULE - nn, // N - k, // base-2^k - res, res_size, res_sl, // res - a, a_size, // a - tmp_space); -} - -EXPORT uint64_t vec_znx_big_normalize_base2k_tmp_bytes(const MODULE* module, uint64_t nn // N -) { - return module->func.vec_znx_big_normalize_base2k_tmp_bytes(module, nn // N - ); -} - -/** @brief sets res = k-normalize(a.subrange) -- output in int64 coeffs space */ -EXPORT void vec_znx_big_range_normalize_base2k( // - const MODULE* module, // MODULE - uint64_t nn, // N - uint64_t log2_base2k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_range_begin, uint64_t a_range_xend, uint64_t a_range_step, // range - uint8_t* tmp_space // temp space -) { - module->func.vec_znx_big_range_normalize_base2k(module, nn, log2_base2k, res, res_size, res_sl, a, a_range_begin, - a_range_xend, a_range_step, tmp_space); -} - -/** @brief returns the minimal byte length of scratch space for vec_znx_big_range_normalize_base2k */ -EXPORT uint64_t vec_znx_big_range_normalize_base2k_tmp_bytes( // - const MODULE* module, // MODULE - uint64_t nn // N -) { - return module->func.vec_znx_big_range_normalize_base2k_tmp_bytes(module, nn); -} - -EXPORT void fft64_vec_znx_big_normalize_base2k(const MODULE* module, // MODULE - uint64_t nn, // N - uint64_t k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_size, // a - uint8_t* tmp_space) { - uint64_t a_sl = nn; - module->func.vec_znx_normalize_base2k(module, // N - nn, - k, // log2_base2k - res, res_size, res_sl, // res - (int64_t*)a, a_size, a_sl, // a - tmp_space); -} - -EXPORT void fft64_vec_znx_big_range_normalize_base2k( // - const MODULE* module, // MODULE - uint64_t nn, // N - uint64_t k, // base-2^k - int64_t* res, uint64_t res_size, uint64_t res_sl, // res - const VEC_ZNX_BIG* a, uint64_t a_begin, uint64_t a_end, uint64_t a_step, // a - uint8_t* tmp_space) { - // convert the range indexes to int64[] slices - const int64_t* a_st = ((int64_t*)a) + nn * a_begin; - const uint64_t a_size = (a_end + a_step - 1 - a_begin) / a_step; - const uint64_t a_sl = nn * a_step; - // forward the call - module->func.vec_znx_normalize_base2k(module, // MODULE - nn, // N - k, // log2_base2k - res, res_size, res_sl, // res - a_st, a_size, a_sl, // a - tmp_space); -}
@@ -1,214 +0,0 @@
-#include <string.h> - -#include "../q120/q120_arithmetic.h" -#include "vec_znx_arithmetic_private.h" - -EXPORT void vec_znx_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - return module->func.vec_znx_dft(module, res, res_size, a, a_size, a_sl); -} - -EXPORT void vec_dft_add(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -) { - return module->func.vec_dft_add(module, res, res_size, a, a_size, b, b_size); -} - -EXPORT void vec_dft_sub(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -) { - return module->func.vec_dft_sub(module, res, res_size, a, a_size, b, b_size); -} - -EXPORT void vec_znx_idft(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - uint8_t* tmp // scratch space -) { - return module->func.vec_znx_idft(module, res, res_size, a_dft, a_size, tmp); -} - -EXPORT uint64_t vec_znx_idft_tmp_bytes(const MODULE* module, uint64_t nn) { return module->func.vec_znx_idft_tmp_bytes(module, nn); } - -EXPORT void vec_znx_idft_tmp_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - VEC_ZNX_DFT* a_dft, uint64_t a_size // a is overwritten -) { - return module->func.vec_znx_idft_tmp_a(module, res, res_size, a_dft, a_size); -} - -EXPORT uint64_t bytes_of_vec_znx_dft(const MODULE* module, // N - uint64_t size) { - return module->func.bytes_of_vec_znx_dft(module, size); -} - -// fft64 backend -EXPORT uint64_t fft64_bytes_of_vec_znx_dft(const MODULE* module, // N - uint64_t size) { - return module->nn * size * sizeof(double); -} - -EXPORT VEC_ZNX_DFT* new_vec_znx_dft(const MODULE* module, // N - uint64_t size) { - return spqlios_alloc(bytes_of_vec_znx_dft(module, size)); -} - -EXPORT void delete_vec_znx_dft(VEC_ZNX_DFT* res) { spqlios_free(res); } - -EXPORT void fft64_vec_znx_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t smin = res_size < a_size ? res_size : a_size; - const uint64_t nn = module->nn; - - for (uint64_t i = 0; i < smin; i++) { - reim_from_znx64(module->mod.fft64.p_conv, ((double*)res) + i * nn, a + i * a_sl); - reim_fft(module->mod.fft64.p_fft, ((double*)res) + i * nn); - } - - // fill up remaining part with 0's - double* const dres = (double*)res; - memset(dres + smin * nn, 0, (res_size - smin) * nn * sizeof(double)); -} - -EXPORT void fft64_vec_dft_add(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -) { - const uint64_t smin0 = a_size < b_size ? a_size : b_size; - const uint64_t smin = res_size < smin0 ? res_size : smin0; - const uint64_t nn = module->nn; - - for (uint64_t i = 0; i < smin; i++) { - reim_fftvec_add(module->mod.fft64.add_fft, ((double*)res) + i * nn, ((double*)a) + i * nn, ((double*)b) + i * nn); - } - - // fill remain `res` part with 0's - double* const dres = (double*)res; - memset(dres + smin * nn, 0, (res_size - smin) * nn * sizeof(double)); -} - -EXPORT void fft64_vec_dft_sub(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a, uint64_t a_size, // a - const VEC_ZNX_DFT* b, uint64_t b_size // b -) { - const uint64_t smin0 = a_size < b_size ? a_size : b_size; - const uint64_t smin = res_size < smin0 ? res_size : smin0; - const uint64_t nn = module->nn; - - for (uint64_t i = 0; i < smin; i++) { - reim_fftvec_sub(module->mod.fft64.sub_fft, ((double*)res) + i * nn, ((double*)a) + i * nn, ((double*)b) + i * nn); - } - - // fill remain `res` part with 0's - double* const dres = (double*)res; - memset(dres + smin * nn, 0, (res_size - smin) * nn * sizeof(double)); -} - -EXPORT void fft64_vec_znx_idft(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - uint8_t* tmp // unused -) { - const uint64_t nn = module->nn; - const uint64_t smin = res_size < a_size ? res_size : a_size; - if ((double*)res != (double*)a_dft) { - memcpy(res, a_dft, smin * nn * sizeof(double)); - } - - for (uint64_t i = 0; i < smin; i++) { - reim_ifft(module->mod.fft64.p_ifft, ((double*)res) + i * nn); - reim_to_znx64(module->mod.fft64.p_reim_to_znx, ((int64_t*)res) + i * nn, ((int64_t*)res) + i * nn); - } - - // fill up remaining part with 0's - int64_t* const dres = (int64_t*)res; - memset(dres + smin * nn, 0, (res_size - smin) * nn * sizeof(double)); -} - -EXPORT uint64_t fft64_vec_znx_idft_tmp_bytes(const MODULE* module, uint64_t nn) { return 0; } - -EXPORT void fft64_vec_znx_idft_tmp_a(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - VEC_ZNX_DFT* a_dft, uint64_t a_size // a is overwritten -) { - const uint64_t nn = module->nn; - const uint64_t smin = res_size < a_size ? res_size : a_size; - - int64_t* const tres = (int64_t*)res; - double* const ta = (double*)a_dft; - for (uint64_t i = 0; i < smin; i++) { - reim_ifft(module->mod.fft64.p_ifft, ta + i * nn); - reim_to_znx64(module->mod.fft64.p_reim_to_znx, tres + i * nn, ta + i * nn); - } - - // fill up remaining part with 0's - memset(tres + smin * nn, 0, (res_size - smin) * nn * sizeof(double)); -} - -// ntt120 backend - -EXPORT void ntt120_vec_znx_dft_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl // a -) { - const uint64_t nn = module->nn; - const uint64_t smin = res_size < a_size ? res_size : a_size; - - int64_t* tres = (int64_t*)res; - for (uint64_t i = 0; i < smin; i++) { - q120_b_from_znx64_simple(nn, (q120b*)(tres + i * nn * 4), a + i * a_sl); - q120_ntt_bb_avx2(module->mod.q120.p_ntt, (q120b*)(tres + i * nn * 4)); - } - - // fill up remaining part with 0's - memset(tres + smin * nn * 4, 0, (res_size - smin) * nn * 4 * sizeof(int64_t)); -} - -EXPORT void ntt120_vec_znx_idft_avx(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - uint8_t* tmp) { - const uint64_t nn = module->nn; - const uint64_t smin = res_size < a_size ? res_size : a_size; - - __int128_t* const tres = (__int128_t*)res; - const int64_t* const ta = (int64_t*)a_dft; - for (uint64_t i = 0; i < smin; i++) { - memcpy(tmp, ta + i * nn * 4, nn * 4 * sizeof(uint64_t)); - q120_intt_bb_avx2(module->mod.q120.p_intt, (q120b*)tmp); - q120_b_to_znx128_simple(nn, tres + i * nn, (q120b*)tmp); - } - - // fill up remaining part with 0's - memset(tres + smin * nn, 0, (res_size - smin) * nn * sizeof(*tres)); -} - -EXPORT uint64_t ntt120_vec_znx_idft_tmp_bytes_avx(const MODULE* module, uint64_t nn) { return nn * 4 * sizeof(uint64_t); } - -EXPORT void ntt120_vec_znx_idft_tmp_a_avx(const MODULE* module, // N - VEC_ZNX_BIG* res, uint64_t res_size, // res - VEC_ZNX_DFT* a_dft, uint64_t a_size // a is overwritten -) { - const uint64_t nn = module->nn; - const uint64_t smin = res_size < a_size ? res_size : a_size; - - __int128_t* const tres = (__int128_t*)res; - int64_t* const ta = (int64_t*)a_dft; - for (uint64_t i = 0; i < smin; i++) { - q120_intt_bb_avx2(module->mod.q120.p_intt, (q120b*)(ta + i * nn * 4)); - q120_b_to_znx128_simple(nn, tres + i * nn, (q120b*)(ta + i * nn * 4)); - } - - // fill up remaining part with 0's - memset(tres + smin * nn, 0, (res_size - smin) * nn * sizeof(*tres)); -}
@@ -1,1 +0,0 @@
-#include "vec_znx_arithmetic_private.h"
@@ -1,369 +0,0 @@
-#include <assert.h> -#include <string.h> - -#include "../reim4/reim4_arithmetic.h" -#include "vec_znx_arithmetic_private.h" - -EXPORT uint64_t bytes_of_vmp_pmat(const MODULE* module, // N - uint64_t nrows, uint64_t ncols // dimensions -) { - return module->func.bytes_of_vmp_pmat(module, nrows, ncols); -} - -// fft64 -EXPORT uint64_t fft64_bytes_of_vmp_pmat(const MODULE* module, // N - uint64_t nrows, uint64_t ncols // dimensions -) { - return module->nn * nrows * ncols * sizeof(double); -} - -EXPORT VMP_PMAT* new_vmp_pmat(const MODULE* module, // N - uint64_t nrows, uint64_t ncols // dimensions -) { - return spqlios_alloc(bytes_of_vmp_pmat(module, nrows, ncols)); -} - -EXPORT void delete_vmp_pmat(VMP_PMAT* res) { spqlios_free(res); } - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void vmp_prepare_contiguous(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - module->func.vmp_prepare_contiguous(module, pmat, mat, nrows, ncols, tmp_space); -} - -/** @brief minimal scratch space byte-size required for the vmp_prepare function */ -EXPORT uint64_t vmp_prepare_tmp_bytes(const MODULE* module, uint64_t nn, // N - uint64_t nrows, uint64_t ncols) { - return module->func.vmp_prepare_tmp_bytes(module, nn, nrows, ncols); -} - -EXPORT double* get_blk_addr(uint64_t row_i, uint64_t col_i, uint64_t nrows, uint64_t ncols, const VMP_PMAT* pmat) { - double* output_mat = (double*)pmat; - - if (col_i == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - return output_mat + col_i * nrows * 8 // col == ncols-1 - + row_i * 8; - } else { - // general case: columns go by pair - return output_mat + (col_i / 2) * (2 * nrows) * 8 // second: col pair index - + row_i * 2 * 8 // third: row index - + (col_i % 2) * 8; - } -} - -void fft64_store_svp_ppol_into_vmp_pmat_row_blk_ref(uint64_t nn, uint64_t m, const SVP_PPOL* svp_ppol, uint64_t row_i, - uint64_t col_i, uint64_t nrows, uint64_t ncols, VMP_PMAT* pmat) { - double* start_addr = get_blk_addr(row_i, col_i, nrows, ncols, pmat); - uint64_t offset = nrows * ncols * 8; - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - reim4_extract_1blk_from_reim_ref(m, blk_i, start_addr + blk_i * offset, (double*)svp_ppol); - } -} - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void fft64_vmp_prepare_contiguous_ref(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - // there is an edge case if nn < 8 - const uint64_t nn = module->nn; - const uint64_t m = module->m; - - if (nn >= 8) { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - reim_from_znx64(module->mod.fft64.p_conv, (SVP_PPOL*)tmp_space, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->mod.fft64.p_fft, (double*)tmp_space); - fft64_store_svp_ppol_into_vmp_pmat_row_blk_ref(nn, m, (SVP_PPOL*)tmp_space, row_i, col_i, nrows, ncols, pmat); - } - } - } else { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - double* res = (double*)pmat + (col_i * nrows + row_i) * nn; - reim_from_znx64(module->mod.fft64.p_conv, (SVP_PPOL*)res, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->mod.fft64.p_fft, res); - } - } - } -} - -/** @brief minimal scratch space byte-size required for the vmp_prepare function */ -EXPORT uint64_t fft64_vmp_prepare_tmp_bytes(const MODULE* module, uint64_t nn, // N - uint64_t nrows, uint64_t ncols) { - return nn * sizeof(int64_t); -} - -/** @brief applies a vmp product (result in DFT space) and adds to res inplace */ -EXPORT void fft64_vmp_apply_dft_add_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, - uint64_t pmat_scale, // prep matrix - uint8_t* tmp_space // scratch space -) { - const uint64_t nn = module->nn; - const uint64_t rows = nrows < a_size ? nrows : a_size; - - VEC_ZNX_DFT* a_dft = (VEC_ZNX_DFT*)tmp_space; - uint8_t* new_tmp_space = (uint8_t*)tmp_space + rows * nn * sizeof(double); - - fft64_vec_znx_dft(module, a_dft, rows, a, a_size, a_sl); - fft64_vmp_apply_dft_to_dft_add_ref(module, res, res_size, a_dft, a_size, pmat, nrows, ncols, pmat_scale, - new_tmp_space); -} - -/** @brief applies a vmp product (result in DFT space) */ -EXPORT void fft64_vmp_apply_dft_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -) { - const uint64_t nn = module->nn; - const uint64_t rows = nrows < a_size ? nrows : a_size; - - VEC_ZNX_DFT* a_dft = (VEC_ZNX_DFT*)tmp_space; - uint8_t* new_tmp_space = (uint8_t*)tmp_space + rows * nn * sizeof(double); - - fft64_vec_znx_dft(module, a_dft, rows, a, a_size, a_sl); - fft64_vmp_apply_dft_to_dft_ref(module, res, res_size, a_dft, a_size, pmat, nrows, ncols, new_tmp_space); -} - -/** @brief like fft64_vmp_apply_dft_to_dft_ref but adds in place */ -EXPORT void fft64_vmp_apply_dft_to_dft_add_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, const uint64_t ncols, - uint64_t pmat_scale, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - const uint64_t m = module->m; - const uint64_t nn = module->nn; - assert(nn >= 8); - - double* mat2cols_output = (double*)tmp_space; // 128 bytes - double* extracted_blk = (double*)tmp_space + 16; // 64*min(nrows,a_size) bytes - - double* mat_input = (double*)pmat; - double* vec_input = (double*)a_dft; - double* vec_output = (double*)res; - - // const uint64_t row_max0 = res_size < a_size ? res_size: a_size; - const uint64_t row_max = nrows < a_size ? nrows : a_size; - const uint64_t col_max = ncols < res_size ? ncols : res_size; - - if (nn >= 8) { - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - double* mat_blk_start = mat_input + blk_i * (8 * nrows * ncols); - - reim4_extract_1blk_from_contiguous_reim_ref(m, row_max, blk_i, (double*)extracted_blk, (double*)a_dft); - - if (pmat_scale % 2 == 0) { - // apply mat2cols - for (uint64_t col_res = 0, col_pmat = pmat_scale; col_pmat < col_max - 1; col_res += 2, col_pmat += 2) { - uint64_t col_offset = col_pmat * (8 * nrows); - reim4_vec_mat2cols_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - reim4_add_1blk_to_reim_ref(m, blk_i, vec_output + col_res * nn, mat2cols_output); - reim4_add_1blk_to_reim_ref(m, blk_i, vec_output + (col_res + 1) * nn, mat2cols_output + 8); - } - } else { - uint64_t col_offset = (pmat_scale - 1) * (8 * nrows); - reim4_vec_mat2cols_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - reim4_add_1blk_to_reim_ref(m, blk_i, vec_output, mat2cols_output + 8); - - // apply mat2cols - for (uint64_t col_res = 1, col_pmat = pmat_scale + 1; col_pmat < col_max - 1; col_res += 2, col_pmat += 2) { - uint64_t col_offset = col_pmat * (8 * nrows); - reim4_vec_mat2cols_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - - reim4_add_1blk_to_reim_ref(m, blk_i, vec_output + col_res * nn, mat2cols_output); - reim4_add_1blk_to_reim_ref(m, blk_i, vec_output + (col_res + 1) * nn, mat2cols_output + 8); - } - } - - // check if col_max is odd, then special case - if (col_max % 2 == 1) { - uint64_t last_col = col_max - 1; - uint64_t col_offset = last_col * (8 * nrows); - - if (last_col >= pmat_scale) { - // the last column is alone in the pmat: vec_mat1col - if (ncols == col_max) { - reim4_vec_mat1col_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } else { - // the last column is part of a colpair in the pmat: vec_mat2cols and ignore the second position - reim4_vec_mat2cols_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } - - reim4_add_1blk_to_reim_ref(m, blk_i, vec_output + (last_col - pmat_scale) * nn, mat2cols_output); - } - } - } - } else { - for (uint64_t col_res = 0, col_pmat = pmat_scale; col_pmat < col_max; col_res += 1, col_pmat += 1) { - double* pmat_col = mat_input + col_pmat * nrows * nn; - for (uint64_t row_i = 0; row_i < row_max; row_i++) { - reim_fftvec_addmul(module->mod.fft64.p_addmul, vec_output + col_res * nn, vec_input + row_i * nn, - pmat_col + row_i * nn); - } - } - } - - // zero out remaining bytes - memset(vec_output + col_max * nn, 0, (res_size - col_max) * nn * sizeof(double)); -} - -/** @brief this inner function could be very handy */ -EXPORT void fft64_vmp_apply_dft_to_dft_ref(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, - const uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - const uint64_t m = module->m; - const uint64_t nn = module->nn; - assert(nn >= 8); - - double* mat2cols_output = (double*)tmp_space; // 128 bytes - double* extracted_blk = (double*)tmp_space + 16; // 64*min(nrows,a_size) bytes - - double* mat_input = (double*)pmat; - double* vec_input = (double*)a_dft; - double* vec_output = (double*)res; - - // const uint64_t row_max0 = res_size < a_size ? res_size: a_size; - const uint64_t row_max = nrows < a_size ? nrows : a_size; - const uint64_t col_max = ncols < res_size ? ncols : res_size; - - if (nn >= 8) { - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - double* mat_blk_start = mat_input + blk_i * (8 * nrows * ncols); - - reim4_extract_1blk_from_contiguous_reim_ref(m, row_max, blk_i, (double*)extracted_blk, (double*)a_dft); - // apply mat2cols - for (uint64_t col_i = 0; col_i < col_max - 1; col_i += 2) { - uint64_t col_offset = col_i * (8 * nrows); - reim4_vec_mat2cols_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - - reim4_save_1blk_to_reim_ref(m, blk_i, vec_output + col_i * nn, mat2cols_output); - reim4_save_1blk_to_reim_ref(m, blk_i, vec_output + (col_i + 1) * nn, mat2cols_output + 8); - } - - // check if col_max is odd, then special case - if (col_max % 2 == 1) { - uint64_t last_col = col_max - 1; - uint64_t col_offset = last_col * (8 * nrows); - - // the last column is alone in the pmat: vec_mat1col - if (ncols == col_max) { - reim4_vec_mat1col_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } else { - // the last column is part of a colpair in the pmat: vec_mat2cols and ignore the second position - reim4_vec_mat2cols_product_ref(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } - reim4_save_1blk_to_reim_ref(m, blk_i, vec_output + last_col * nn, mat2cols_output); - } - } - } else { - for (uint64_t col_i = 0; col_i < col_max; col_i++) { - double* pmat_col = mat_input + col_i * nrows * nn; - for (uint64_t row_i = 0; row_i < 1; row_i++) { - reim_fftvec_mul(module->mod.fft64.mul_fft, vec_output + col_i * nn, vec_input + row_i * nn, - pmat_col + row_i * nn); - } - for (uint64_t row_i = 1; row_i < row_max; row_i++) { - reim_fftvec_addmul(module->mod.fft64.p_addmul, vec_output + col_i * nn, vec_input + row_i * nn, - pmat_col + row_i * nn); - } - } - } - - // zero out remaining bytes - memset(vec_output + col_max * nn, 0, (res_size - col_max) * nn * sizeof(double)); -} - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t fft64_vmp_apply_dft_tmp_bytes(const MODULE* module, uint64_t nn, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - const uint64_t row_max = nrows < a_size ? nrows : a_size; - return (row_max * nn * sizeof(double)) + (128) + (64 * row_max); -} - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t fft64_vmp_apply_dft_to_dft_tmp_bytes(const MODULE* module, uint64_t nn, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - const uint64_t row_max = nrows < a_size ? nrows : a_size; - - return (128) + (64 * row_max); -} - -EXPORT void vmp_apply_dft_to_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, - const uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - module->func.vmp_apply_dft_to_dft(module, res, res_size, a_dft, a_size, pmat, nrows, ncols, tmp_space); -} - -EXPORT void vmp_apply_dft_to_dft_add(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, const uint64_t ncols, - uint64_t pmat_scale, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - module->func.vmp_apply_dft_to_dft_add(module, res, res_size, a_dft, a_size, pmat, nrows, ncols, pmat_scale, - tmp_space); -} - -EXPORT uint64_t vmp_apply_dft_to_dft_tmp_bytes(const MODULE* module, uint64_t nn, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - return module->func.vmp_apply_dft_to_dft_tmp_bytes(module, nn, res_size, a_size, nrows, ncols); -} - -/** @brief applies a vmp product (result in DFT space) adds to res inplace */ -EXPORT void vmp_apply_dft_add(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, uint64_t pmat_scale, // prep matrix - uint8_t* tmp_space // scratch space -) { - module->func.vmp_apply_dft_add(module, res, res_size, a, a_size, a_sl, pmat, nrows, ncols, pmat_scale, tmp_space); -} - -/** @brief applies a vmp product (result in DFT space) */ -EXPORT void vmp_apply_dft(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -) { - module->func.vmp_apply_dft(module, res, res_size, a, a_size, a_sl, pmat, nrows, ncols, tmp_space); -} - -/** @brief minimal size of the tmp_space */ -EXPORT uint64_t vmp_apply_dft_tmp_bytes(const MODULE* module, uint64_t nn, // N - uint64_t res_size, // res - uint64_t a_size, // a - uint64_t nrows, uint64_t ncols // prep matrix -) { - return module->func.vmp_apply_dft_tmp_bytes(module, nn, res_size, a_size, nrows, ncols); -}
@@ -1,244 +0,0 @@
-#include <string.h> - -#include "../reim4/reim4_arithmetic.h" -#include "vec_znx_arithmetic_private.h" - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void fft64_vmp_prepare_contiguous_avx(const MODULE* module, // N - VMP_PMAT* pmat, // output - const int64_t* mat, uint64_t nrows, uint64_t ncols, // a - uint8_t* tmp_space // scratch space -) { - // there is an edge case if nn < 8 - const uint64_t nn = module->nn; - const uint64_t m = module->m; - - double* output_mat = (double*)pmat; - double* start_addr = (double*)pmat; - uint64_t offset = nrows * ncols * 8; - - if (nn >= 8) { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - reim_from_znx64(module->mod.fft64.p_conv, (SVP_PPOL*)tmp_space, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->mod.fft64.p_fft, (double*)tmp_space); - - if (col_i == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - start_addr = output_mat + col_i * nrows * 8 // col == ncols-1 - + row_i * 8; - } else { - // general case: columns go by pair - start_addr = output_mat + (col_i / 2) * (2 * nrows) * 8 // second: col pair index - + row_i * 2 * 8 // third: row index - + (col_i % 2) * 8; - } - - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - // extract blk from tmp and save it - reim4_extract_1blk_from_reim_avx(m, blk_i, start_addr + blk_i * offset, (double*)tmp_space); - } - } - } - } else { - for (uint64_t row_i = 0; row_i < nrows; row_i++) { - for (uint64_t col_i = 0; col_i < ncols; col_i++) { - double* res = (double*)pmat + (col_i * nrows + row_i) * nn; - reim_from_znx64(module->mod.fft64.p_conv, (SVP_PPOL*)res, mat + (row_i * ncols + col_i) * nn); - reim_fft(module->mod.fft64.p_fft, res); - } - } - } -} - -double* get_blk_addr(int row, int col, int nrows, int ncols, VMP_PMAT* pmat); - -void fft64_store_svp_ppol_into_vmp_pmat_row_blk_avx(uint64_t nn, uint64_t m, const SVP_PPOL* svp_ppol, uint64_t row_i, - uint64_t col_i, uint64_t nrows, uint64_t ncols, VMP_PMAT* pmat) { - double* start_addr = get_blk_addr(row_i, col_i, nrows, ncols, pmat); - uint64_t offset = nrows * ncols * 8; - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - reim4_extract_1blk_from_reim_avx(m, blk_i, start_addr + blk_i * offset, (double*)svp_ppol); - } -} - -/** @brief applies a vmp product (result in DFT space) abd adds to res inplace */ -EXPORT void fft64_vmp_apply_dft_add_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, - uint64_t pmat_scale, // prep matrix - uint8_t* tmp_space // scratch space -) { - const uint64_t nn = module->nn; - const uint64_t rows = nrows < a_size ? nrows : a_size; - - VEC_ZNX_DFT* a_dft = (VEC_ZNX_DFT*)tmp_space; - uint8_t* new_tmp_space = (uint8_t*)tmp_space + rows * nn * sizeof(double); - - fft64_vec_znx_dft(module, a_dft, rows, a, a_size, a_sl); - fft64_vmp_apply_dft_to_dft_add_avx(module, res, res_size, a_dft, a_size, pmat, nrows, ncols, pmat_scale, - new_tmp_space); -} - -/** @brief applies a vmp product (result in DFT space) */ -EXPORT void fft64_vmp_apply_dft_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size, uint64_t a_sl, // a - const VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space -) { - const uint64_t nn = module->nn; - const uint64_t rows = nrows < a_size ? nrows : a_size; - - VEC_ZNX_DFT* a_dft = (VEC_ZNX_DFT*)tmp_space; - uint8_t* new_tmp_space = (uint8_t*)tmp_space + rows * nn * sizeof(double); - - fft64_vec_znx_dft(module, a_dft, rows, a, a_size, a_sl); - fft64_vmp_apply_dft_to_dft_avx(module, res, res_size, a_dft, a_size, pmat, nrows, ncols, new_tmp_space); -} - -EXPORT void fft64_vmp_apply_dft_to_dft_add_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, const uint64_t ncols, - uint64_t pmat_scale, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - const uint64_t m = module->m; - const uint64_t nn = module->nn; - - double* mat2cols_output = (double*)tmp_space; // 128 bytes - double* extracted_blk = (double*)tmp_space + 16; // 64*min(nrows,a_size) bytes - - double* mat_input = (double*)pmat; - double* vec_input = (double*)a_dft; - double* vec_output = (double*)res; - - const uint64_t row_max = nrows < a_size ? nrows : a_size; - const uint64_t col_max = ncols < res_size ? ncols : res_size; - - if (nn >= 8) { - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - double* mat_blk_start = mat_input + blk_i * (8 * nrows * ncols); - - reim4_extract_1blk_from_contiguous_reim_avx(m, row_max, blk_i, (double*)extracted_blk, (double*)a_dft); - - if (pmat_scale % 2 == 0) { - for (uint64_t col_res = 0, col_pmat = pmat_scale; col_pmat < col_max - 1; col_res += 2, col_pmat += 2) { - uint64_t col_offset = col_pmat * (8 * nrows); - reim4_vec_mat2cols_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - reim4_add_1blk_to_reim_avx(m, blk_i, vec_output + col_res * nn, mat2cols_output); - reim4_add_1blk_to_reim_avx(m, blk_i, vec_output + (col_res + 1) * nn, mat2cols_output + 8); - } - } else { - uint64_t col_offset = (pmat_scale - 1) * (8 * nrows); - reim4_vec_mat2cols_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - reim4_add_1blk_to_reim_avx(m, blk_i, vec_output, mat2cols_output + 8); - - for (uint64_t col_res = 1, col_pmat = pmat_scale + 1; col_pmat < col_max - 1; col_res += 2, col_pmat += 2) { - uint64_t col_offset = col_pmat * (8 * nrows); - reim4_vec_mat2cols_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - reim4_add_1blk_to_reim_avx(m, blk_i, vec_output + col_res * nn, mat2cols_output); - reim4_add_1blk_to_reim_avx(m, blk_i, vec_output + (col_res + 1) * nn, mat2cols_output + 8); - } - } - - // check if col_max is odd, then special case - if (col_max % 2 == 1) { - uint64_t last_col = col_max - 1; - uint64_t col_offset = last_col * (8 * nrows); - - if (last_col >= pmat_scale) { - // the last column is alone in the pmat: vec_mat1col - if (ncols == col_max) - reim4_vec_mat1col_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - else { - // the last column is part of a colpair in the pmat: vec_mat2cols and ignore the second position - reim4_vec_mat2cols_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } - reim4_add_1blk_to_reim_avx(m, blk_i, vec_output + (last_col - pmat_scale) * nn, mat2cols_output); - } - } - } - } else { - for (uint64_t col_res = 0, col_pmat = pmat_scale; col_pmat < col_max; col_res += 1, col_pmat += 1) { - double* pmat_col = mat_input + col_pmat * nrows * nn; - for (uint64_t row_i = 0; row_i < row_max; row_i++) { - reim_fftvec_addmul(module->mod.fft64.p_addmul, vec_output + col_res * nn, vec_input + row_i * nn, - pmat_col + row_i * nn); - } - } - } - - // zero out remaining bytes - memset(vec_output + col_max * nn, 0, (res_size - col_max) * nn * sizeof(double)); -} - -/** @brief this inner function could be very handy */ -EXPORT void fft64_vmp_apply_dft_to_dft_avx(const MODULE* module, // N - VEC_ZNX_DFT* res, const uint64_t res_size, // res - const VEC_ZNX_DFT* a_dft, uint64_t a_size, // a - const VMP_PMAT* pmat, const uint64_t nrows, - const uint64_t ncols, // prep matrix - uint8_t* tmp_space // scratch space (a_size*sizeof(reim4) bytes) -) { - const uint64_t m = module->m; - const uint64_t nn = module->nn; - - double* mat2cols_output = (double*)tmp_space; // 128 bytes - double* extracted_blk = (double*)tmp_space + 16; // 64*min(nrows,a_size) bytes - - double* mat_input = (double*)pmat; - double* vec_input = (double*)a_dft; - double* vec_output = (double*)res; - - const uint64_t row_max = nrows < a_size ? nrows : a_size; - const uint64_t col_max = ncols < res_size ? ncols : res_size; - - if (nn >= 8) { - for (uint64_t blk_i = 0; blk_i < m / 4; blk_i++) { - double* mat_blk_start = mat_input + blk_i * (8 * nrows * ncols); - - reim4_extract_1blk_from_contiguous_reim_avx(m, row_max, blk_i, (double*)extracted_blk, (double*)a_dft); - // apply mat2cols - for (uint64_t col_i = 0; col_i < col_max - 1; col_i += 2) { - uint64_t col_offset = col_i * (8 * nrows); - reim4_vec_mat2cols_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - - reim4_save_1blk_to_reim_avx(m, blk_i, vec_output + col_i * nn, mat2cols_output); - reim4_save_1blk_to_reim_avx(m, blk_i, vec_output + (col_i + 1) * nn, mat2cols_output + 8); - } - - // check if col_max is odd, then special case - if (col_max % 2 == 1) { - uint64_t last_col = col_max - 1; - uint64_t col_offset = last_col * (8 * nrows); - - // the last column is alone in the pmat: vec_mat1col - if (ncols == col_max) - reim4_vec_mat1col_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - else { - // the last column is part of a colpair in the pmat: vec_mat2cols and ignore the second position - reim4_vec_mat2cols_product_avx2(row_max, mat2cols_output, extracted_blk, mat_blk_start + col_offset); - } - reim4_save_1blk_to_reim_avx(m, blk_i, vec_output + last_col * nn, mat2cols_output); - } - } - } else { - for (uint64_t col_i = 0; col_i < col_max; col_i++) { - double* pmat_col = mat_input + col_i * nrows * nn; - for (uint64_t row_i = 0; row_i < 1; row_i++) { - reim_fftvec_mul(module->mod.fft64.mul_fft, vec_output + col_i * nn, vec_input + row_i * nn, - pmat_col + row_i * nn); - } - for (uint64_t row_i = 1; row_i < row_max; row_i++) { - reim_fftvec_addmul(module->mod.fft64.p_addmul, vec_output + col_i * nn, vec_input + row_i * nn, - pmat_col + row_i * nn); - } - } - } - - // zero out remaining bytes - memset(vec_output + col_max * nn, 0, (res_size - col_max) * nn * sizeof(double)); -}
@@ -1,185 +0,0 @@
-#include <string.h> - -#include "zn_arithmetic_private.h" - -void default_init_z_module_precomp(MOD_Z* module) { - // Add here initialization of items that are in the precomp -} - -void default_finalize_z_module_precomp(MOD_Z* module) { - // Add here deleters for items that are in the precomp -} - -void default_init_z_module_vtable(MOD_Z* module) { - // Add function pointers here - module->vtable.i8_approxdecomp_from_tndbl = default_i8_approxdecomp_from_tndbl_ref; - module->vtable.i16_approxdecomp_from_tndbl = default_i16_approxdecomp_from_tndbl_ref; - module->vtable.i32_approxdecomp_from_tndbl = default_i32_approxdecomp_from_tndbl_ref; - module->vtable.zn32_vmp_prepare_contiguous = default_zn32_vmp_prepare_contiguous_ref; - module->vtable.zn32_vmp_prepare_dblptr = default_zn32_vmp_prepare_dblptr_ref; - module->vtable.zn32_vmp_prepare_row = default_zn32_vmp_prepare_row_ref; - module->vtable.zn32_vmp_apply_i8 = default_zn32_vmp_apply_i8_ref; - module->vtable.zn32_vmp_apply_i16 = default_zn32_vmp_apply_i16_ref; - module->vtable.zn32_vmp_apply_i32 = default_zn32_vmp_apply_i32_ref; - module->vtable.dbl_to_tn32 = dbl_to_tn32_ref; - module->vtable.tn32_to_dbl = tn32_to_dbl_ref; - module->vtable.dbl_round_to_i32 = dbl_round_to_i32_ref; - module->vtable.i32_to_dbl = i32_to_dbl_ref; - module->vtable.dbl_round_to_i64 = dbl_round_to_i64_ref; - module->vtable.i64_to_dbl = i64_to_dbl_ref; - - // Add optimized function pointers here - if (CPU_SUPPORTS("avx")) { - module->vtable.zn32_vmp_apply_i8 = default_zn32_vmp_apply_i8_avx; - module->vtable.zn32_vmp_apply_i16 = default_zn32_vmp_apply_i16_avx; - module->vtable.zn32_vmp_apply_i32 = default_zn32_vmp_apply_i32_avx; - } -} - -void init_z_module_info(MOD_Z* module, // - Z_MODULE_TYPE mtype) { - memset(module, 0, sizeof(MOD_Z)); - module->mtype = mtype; - switch (mtype) { - case DEFAULT: - default_init_z_module_precomp(module); - default_init_z_module_vtable(module); - break; - default: - NOT_SUPPORTED(); // unknown mtype - } -} - -void finalize_z_module_info(MOD_Z* module) { - if (module->custom) module->custom_deleter(module->custom); - switch (module->mtype) { - case DEFAULT: - default_finalize_z_module_precomp(module); - // fft64_finalize_rnx_module_vtable(module); // nothing to finalize - break; - default: - NOT_SUPPORTED(); // unknown mtype - } -} - -EXPORT MOD_Z* new_z_module_info(Z_MODULE_TYPE mtype) { - MOD_Z* res = (MOD_Z*)malloc(sizeof(MOD_Z)); - init_z_module_info(res, mtype); - return res; -} - -EXPORT void delete_z_module_info(MOD_Z* module_info) { - finalize_z_module_info(module_info); - free(module_info); -} - -//////////////// wrappers ////////////////// - -/** @brief sets res = gadget_decompose(a) (int8_t* output) */ -EXPORT void i8_approxdecomp_from_tndbl(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int8_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size) { // a - module->vtable.i8_approxdecomp_from_tndbl(module, gadget, res, res_size, a, a_size); -} - -/** @brief sets res = gadget_decompose(a) (int16_t* output) */ -EXPORT void i16_approxdecomp_from_tndbl(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int16_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size) { // a - module->vtable.i16_approxdecomp_from_tndbl(module, gadget, res, res_size, a, a_size); -} -/** @brief sets res = gadget_decompose(a) (int32_t* output) */ -EXPORT void i32_approxdecomp_from_tndbl(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int32_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size) { // a - module->vtable.i32_approxdecomp_from_tndbl(module, gadget, res, res_size, a, a_size); -} - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void zn32_vmp_prepare_contiguous(const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t* mat, uint64_t nrows, uint64_t ncols) { // a - module->vtable.zn32_vmp_prepare_contiguous(module, pmat, mat, nrows, ncols); -} - -/** @brief prepares a vmp matrix (mat[row]+col*N points to the item) */ -EXPORT void zn32_vmp_prepare_dblptr(const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t** mat, uint64_t nrows, uint64_t ncols) { // a - module->vtable.zn32_vmp_prepare_dblptr(module, pmat, mat, nrows, ncols); -} - -/** @brief prepares the ith-row of a vmp matrix with nrows and ncols */ -EXPORT void zn32_vmp_prepare_row(const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t* row, uint64_t row_i, uint64_t nrows, uint64_t ncols) { // a - module->vtable.zn32_vmp_prepare_row(module, pmat, row, row_i, nrows, ncols); -} - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void zn32_vmp_apply_i32(const MOD_Z* module, int32_t* res, uint64_t res_size, const int32_t* a, uint64_t a_size, - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols) { - module->vtable.zn32_vmp_apply_i32(module, res, res_size, a, a_size, pmat, nrows, ncols); -} -/** @brief applies a vmp product (int16_t* input) */ -EXPORT void zn32_vmp_apply_i16(const MOD_Z* module, int32_t* res, uint64_t res_size, const int16_t* a, uint64_t a_size, - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols) { - module->vtable.zn32_vmp_apply_i16(module, res, res_size, a, a_size, pmat, nrows, ncols); -} - -/** @brief applies a vmp product (int8_t* input) */ -EXPORT void zn32_vmp_apply_i8(const MOD_Z* module, int32_t* res, uint64_t res_size, const int8_t* a, uint64_t a_size, - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols) { - module->vtable.zn32_vmp_apply_i8(module, res, res_size, a, a_size, pmat, nrows, ncols); -} - -/** reduction mod 1, output in torus32 space */ -EXPORT void dbl_to_tn32(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - module->vtable.dbl_to_tn32(module, res, res_size, a, a_size); -} - -/** real centerlift mod 1, output in double space */ -EXPORT void tn32_to_dbl(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -) { - module->vtable.tn32_to_dbl(module, res, res_size, a, a_size); -} - -/** round to the nearest int, output in i32 space */ -EXPORT void dbl_round_to_i32(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - module->vtable.dbl_round_to_i32(module, res, res_size, a, a_size); -} - -/** small int (int32 space) to double */ -EXPORT void i32_to_dbl(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -) { - module->vtable.i32_to_dbl(module, res, res_size, a, a_size); -} - -/** round to the nearest int, output in int64 space */ -EXPORT void dbl_round_to_i64(const MOD_Z* module, // - int64_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - module->vtable.dbl_round_to_i64(module, res, res_size, a, a_size); -} - -/** small int (int64 space, <= 2^50) to double */ -EXPORT void i64_to_dbl(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size // a -) { - module->vtable.i64_to_dbl(module, res, res_size, a, a_size); -}
@@ -1,81 +0,0 @@
-#include <memory.h> - -#include "zn_arithmetic_private.h" - -EXPORT TNDBL_APPROXDECOMP_GADGET* new_tndbl_approxdecomp_gadget(const MOD_Z* module, // - uint64_t k, uint64_t ell) { - if (k * ell > 50) { - return spqlios_error("approx decomposition requested is too precise for doubles"); - } - if (k < 1) { - return spqlios_error("approx decomposition supports k>=1"); - } - TNDBL_APPROXDECOMP_GADGET* res = malloc(sizeof(TNDBL_APPROXDECOMP_GADGET)); - memset(res, 0, sizeof(TNDBL_APPROXDECOMP_GADGET)); - res->k = k; - res->ell = ell; - double add_cst = INT64_C(3) << (51 - k * ell); - for (uint64_t i = 0; i < ell; ++i) { - add_cst += pow(2., -(double)(i * k + 1)); - } - res->add_cst = add_cst; - res->and_mask = (UINT64_C(1) << k) - 1; - res->sub_cst = UINT64_C(1) << (k - 1); - for (uint64_t i = 0; i < ell; ++i) res->rshifts[i] = (ell - 1 - i) * k; - return res; -} -EXPORT void delete_tndbl_approxdecomp_gadget(TNDBL_APPROXDECOMP_GADGET* ptr) { free(ptr); } - -EXPORT int default_init_tndbl_approxdecomp_gadget(const MOD_Z* module, // - TNDBL_APPROXDECOMP_GADGET* res, // - uint64_t k, uint64_t ell) { - return 0; -} - -typedef union { - double dv; - uint64_t uv; -} du_t; - -#define IMPL_ixx_approxdecomp_from_tndbl_ref(ITYPE) \ - if (res_size != a_size * gadget->ell) NOT_IMPLEMENTED(); \ - const uint64_t ell = gadget->ell; \ - const double add_cst = gadget->add_cst; \ - const uint8_t* const rshifts = gadget->rshifts; \ - const ITYPE and_mask = gadget->and_mask; \ - const ITYPE sub_cst = gadget->sub_cst; \ - ITYPE* rr = res; \ - const double* aa = a; \ - const double* aaend = a + a_size; \ - while (aa < aaend) { \ - du_t t = {.dv = *aa + add_cst}; \ - for (uint64_t i = 0; i < ell; ++i) { \ - ITYPE v = (ITYPE)(t.uv >> rshifts[i]); \ - *rr = (v & and_mask) - sub_cst; \ - ++rr; \ - } \ - ++aa; \ - } - -/** @brief sets res = gadget_decompose(a) (int8_t* output) */ -EXPORT void default_i8_approxdecomp_from_tndbl_ref(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int8_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size // -){IMPL_ixx_approxdecomp_from_tndbl_ref(int8_t)} - -/** @brief sets res = gadget_decompose(a) (int16_t* output) */ -EXPORT void default_i16_approxdecomp_from_tndbl_ref(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int16_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -){IMPL_ixx_approxdecomp_from_tndbl_ref(int16_t)} - -/** @brief sets res = gadget_decompose(a) (int32_t* output) */ -EXPORT void default_i32_approxdecomp_from_tndbl_ref(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - IMPL_ixx_approxdecomp_from_tndbl_ref(int32_t) -}
@@ -1,147 +0,0 @@
-#ifndef SPQLIOS_ZN_ARITHMETIC_H -#define SPQLIOS_ZN_ARITHMETIC_H - -#include <stdint.h> - -#include "../commons.h" - -typedef enum z_module_type_t { DEFAULT } Z_MODULE_TYPE; - -/** @brief opaque structure that describes the module and the hardware */ -typedef struct z_module_info_t MOD_Z; - -/** - * @brief obtain a module info for ring dimension N - * the module-info knows about: - * - the dimension N (or the complex dimension m=N/2) - * - any moduleuted fft or ntt items - * - the hardware (avx, arm64, x86, ...) - */ -EXPORT MOD_Z* new_z_module_info(Z_MODULE_TYPE mode); -EXPORT void delete_z_module_info(MOD_Z* module_info); - -typedef struct tndbl_approxdecomp_gadget_t TNDBL_APPROXDECOMP_GADGET; - -EXPORT TNDBL_APPROXDECOMP_GADGET* new_tndbl_approxdecomp_gadget(const MOD_Z* module, // - uint64_t k, - uint64_t ell); // base 2^k, and size - -EXPORT void delete_tndbl_approxdecomp_gadget(TNDBL_APPROXDECOMP_GADGET* ptr); - -/** @brief sets res = gadget_decompose(a) (int8_t* output) */ -EXPORT void i8_approxdecomp_from_tndbl(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int8_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size); // a - -/** @brief sets res = gadget_decompose(a) (int16_t* output) */ -EXPORT void i16_approxdecomp_from_tndbl(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int16_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size); // a -/** @brief sets res = gadget_decompose(a) (int32_t* output) */ -EXPORT void i32_approxdecomp_from_tndbl(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int32_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size); // a - -/** @brief opaque type that represents a prepared matrix */ -typedef struct zn32_vmp_pmat_t ZN32_VMP_PMAT; - -/** @brief size in bytes of a prepared matrix (for custom allocation) */ -EXPORT uint64_t bytes_of_zn32_vmp_pmat(const MOD_Z* module, // N - uint64_t nrows, uint64_t ncols); // dimensions - -/** @brief allocates a prepared matrix (release with delete_zn32_vmp_pmat) */ -EXPORT ZN32_VMP_PMAT* new_zn32_vmp_pmat(const MOD_Z* module, // N - uint64_t nrows, uint64_t ncols); // dimensions - -/** @brief deletes a prepared matrix (release with free) */ -EXPORT void delete_zn32_vmp_pmat(ZN32_VMP_PMAT* ptr); // dimensions - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void zn32_vmp_prepare_contiguous( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t* mat, uint64_t nrows, uint64_t ncols); // a - -/** @brief prepares a vmp matrix (mat[row]+col*N points to the item) */ -EXPORT void zn32_vmp_prepare_dblptr( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t** mat, uint64_t nrows, uint64_t ncols); // a - -/** @brief prepares a vmp matrix (mat[row]+col*N points to the item) */ -EXPORT void zn32_vmp_prepare_row( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t* row, uint64_t row_i, uint64_t nrows, uint64_t ncols); // a - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void zn32_vmp_apply_i32( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int16_t* input) */ -EXPORT void zn32_vmp_apply_i16( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const int16_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int8_t* input) */ -EXPORT void zn32_vmp_apply_i8( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const int8_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -// explicit conversions - -/** reduction mod 1, output in torus32 space */ -EXPORT void dbl_to_tn32(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -); - -/** real centerlift mod 1, output in double space */ -EXPORT void tn32_to_dbl(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -); - -/** round to the nearest int, output in i32 space. - * WARNING: ||a||_inf must be <= 2^18 in this function - */ -EXPORT void dbl_round_to_i32(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -); - -/** small int (int32 space) to double - * WARNING: ||a||_inf must be <= 2^18 in this function - */ -EXPORT void i32_to_dbl(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -); - -/** round to the nearest int, output in int64 space - * WARNING: ||a||_inf must be <= 2^50 in this function - */ -EXPORT void dbl_round_to_i64(const MOD_Z* module, // - int64_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -); - -/** small int (int64 space, <= 2^50) to double - * WARNING: ||a||_inf must be <= 2^50 in this function - */ -EXPORT void i64_to_dbl(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size // a -); - -#endif // SPQLIOS_ZN_ARITHMETIC_H
@@ -1,43 +0,0 @@
-#ifndef SPQLIOS_ZN_ARITHMETIC_PLUGIN_H -#define SPQLIOS_ZN_ARITHMETIC_PLUGIN_H - -#include "zn_arithmetic.h" - -typedef typeof(i8_approxdecomp_from_tndbl) I8_APPROXDECOMP_FROM_TNDBL_F; -typedef typeof(i16_approxdecomp_from_tndbl) I16_APPROXDECOMP_FROM_TNDBL_F; -typedef typeof(i32_approxdecomp_from_tndbl) I32_APPROXDECOMP_FROM_TNDBL_F; -typedef typeof(bytes_of_zn32_vmp_pmat) BYTES_OF_ZN32_VMP_PMAT_F; -typedef typeof(zn32_vmp_prepare_contiguous) ZN32_VMP_PREPARE_CONTIGUOUS_F; -typedef typeof(zn32_vmp_prepare_dblptr) ZN32_VMP_PREPARE_DBLPTR_F; -typedef typeof(zn32_vmp_prepare_row) ZN32_VMP_PREPARE_ROW_F; -typedef typeof(zn32_vmp_apply_i32) ZN32_VMP_APPLY_I32_F; -typedef typeof(zn32_vmp_apply_i16) ZN32_VMP_APPLY_I16_F; -typedef typeof(zn32_vmp_apply_i8) ZN32_VMP_APPLY_I8_F; -typedef typeof(dbl_to_tn32) DBL_TO_TN32_F; -typedef typeof(tn32_to_dbl) TN32_TO_DBL_F; -typedef typeof(dbl_round_to_i32) DBL_ROUND_TO_I32_F; -typedef typeof(i32_to_dbl) I32_TO_DBL_F; -typedef typeof(dbl_round_to_i64) DBL_ROUND_TO_I64_F; -typedef typeof(i64_to_dbl) I64_TO_DBL_F; - -typedef struct z_module_vtable_t Z_MODULE_VTABLE; -struct z_module_vtable_t { - I8_APPROXDECOMP_FROM_TNDBL_F* i8_approxdecomp_from_tndbl; - I16_APPROXDECOMP_FROM_TNDBL_F* i16_approxdecomp_from_tndbl; - I32_APPROXDECOMP_FROM_TNDBL_F* i32_approxdecomp_from_tndbl; - BYTES_OF_ZN32_VMP_PMAT_F* bytes_of_zn32_vmp_pmat; - ZN32_VMP_PREPARE_CONTIGUOUS_F* zn32_vmp_prepare_contiguous; - ZN32_VMP_PREPARE_DBLPTR_F* zn32_vmp_prepare_dblptr; - ZN32_VMP_PREPARE_ROW_F* zn32_vmp_prepare_row; - ZN32_VMP_APPLY_I32_F* zn32_vmp_apply_i32; - ZN32_VMP_APPLY_I16_F* zn32_vmp_apply_i16; - ZN32_VMP_APPLY_I8_F* zn32_vmp_apply_i8; - DBL_TO_TN32_F* dbl_to_tn32; - TN32_TO_DBL_F* tn32_to_dbl; - DBL_ROUND_TO_I32_F* dbl_round_to_i32; - I32_TO_DBL_F* i32_to_dbl; - DBL_ROUND_TO_I64_F* dbl_round_to_i64; - I64_TO_DBL_F* i64_to_dbl; -}; - -#endif // SPQLIOS_ZN_ARITHMETIC_PLUGIN_H
@@ -1,164 +0,0 @@
-#ifndef SPQLIOS_ZN_ARITHMETIC_PRIVATE_H -#define SPQLIOS_ZN_ARITHMETIC_PRIVATE_H - -#include "../commons_private.h" -#include "zn_arithmetic.h" -#include "zn_arithmetic_plugin.h" - -typedef struct main_z_module_precomp_t MAIN_Z_MODULE_PRECOMP; -struct main_z_module_precomp_t { - // TODO -}; - -typedef union z_module_precomp_t Z_MODULE_PRECOMP; -union z_module_precomp_t { - MAIN_Z_MODULE_PRECOMP main; -}; - -void main_init_z_module_precomp(MOD_Z* module); - -void main_finalize_z_module_precomp(MOD_Z* module); - -/** @brief opaque structure that describes the modules (RnX,ZnX,TnX) and the hardware */ -struct z_module_info_t { - Z_MODULE_TYPE mtype; - Z_MODULE_VTABLE vtable; - Z_MODULE_PRECOMP precomp; - void* custom; - void (*custom_deleter)(void*); -}; - -void init_z_module_info(MOD_Z* module, Z_MODULE_TYPE mtype); - -void main_init_z_module_vtable(MOD_Z* module); - -struct tndbl_approxdecomp_gadget_t { - uint64_t k; - uint64_t ell; - double add_cst; // 3.2^51-(K.ell) + 1/2.(sum 2^-(i+1)K) - int64_t and_mask; // (2^K)-1 - int64_t sub_cst; // 2^(K-1) - uint8_t rshifts[64]; // 2^(ell-1-i).K for i in [0:ell-1] -}; - -/** @brief sets res = gadget_decompose(a) (int8_t* output) */ -EXPORT void default_i8_approxdecomp_from_tndbl_ref(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int8_t* res, uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size); // a - -/** @brief sets res = gadget_decompose(a) (int16_t* output) */ -EXPORT void default_i16_approxdecomp_from_tndbl_ref(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int16_t* res, - uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size); // a -/** @brief sets res = gadget_decompose(a) (int32_t* output) */ -EXPORT void default_i32_approxdecomp_from_tndbl_ref(const MOD_Z* module, // N - const TNDBL_APPROXDECOMP_GADGET* gadget, // gadget - int32_t* res, - uint64_t res_size, // res (in general, size ell.a_size) - const double* a, uint64_t a_size); // a - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void default_zn32_vmp_prepare_contiguous_ref( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t* mat, uint64_t nrows, uint64_t ncols // a -); - -/** @brief prepares a vmp matrix (mat[row]+col*N points to the item) */ -EXPORT void default_zn32_vmp_prepare_dblptr_ref( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t** mat, uint64_t nrows, uint64_t ncols // a -); - -/** @brief prepares the ith-row of a vmp matrix with nrows and ncols */ -EXPORT void default_zn32_vmp_prepare_row_ref( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t* row, uint64_t row_i, uint64_t nrows, uint64_t ncols // a -); - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void default_zn32_vmp_apply_i32_ref( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int16_t* input) */ -EXPORT void default_zn32_vmp_apply_i16_ref( // - const MOD_Z* module, // N - int32_t* res, uint64_t res_size, // res - const int16_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int8_t* input) */ -EXPORT void default_zn32_vmp_apply_i8_ref( // - const MOD_Z* module, // N - int32_t* res, uint64_t res_size, // res - const int8_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void default_zn32_vmp_apply_i32_avx( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int16_t* input) */ -EXPORT void default_zn32_vmp_apply_i16_avx( // - const MOD_Z* module, // N - int32_t* res, uint64_t res_size, // res - const int16_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -/** @brief applies a vmp product (int8_t* input) */ -EXPORT void default_zn32_vmp_apply_i8_avx( // - const MOD_Z* module, // N - int32_t* res, uint64_t res_size, // res - const int8_t* a, uint64_t a_size, // a - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols); // prep matrix - -// explicit conversions - -/** reduction mod 1, output in torus32 space */ -EXPORT void dbl_to_tn32_ref(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -); - -/** real centerlift mod 1, output in double space */ -EXPORT void tn32_to_dbl_ref(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -); - -/** round to the nearest int, output in i32 space */ -EXPORT void dbl_round_to_i32_ref(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -); - -/** small int (int32 space) to double */ -EXPORT void i32_to_dbl_ref(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -); - -/** round to the nearest int, output in int64 space */ -EXPORT void dbl_round_to_i64_ref(const MOD_Z* module, // - int64_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -); - -/** small int (int64 space) to double */ -EXPORT void i64_to_dbl_ref(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size // a -); - -#endif // SPQLIOS_ZN_ARITHMETIC_PRIVATE_H
@@ -1,108 +0,0 @@
-#include <memory.h> - -#include "zn_arithmetic_private.h" - -typedef union { - double dv; - int64_t s64v; - int32_t s32v; - uint64_t u64v; - uint32_t u32v; -} di_t; - -/** reduction mod 1, output in torus32 space */ -EXPORT void dbl_to_tn32_ref(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - static const double ADD_CST = 0.5 + (double)(INT64_C(3) << (51 - 32)); - static const int32_t XOR_CST = (INT32_C(1) << 31); - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - di_t t = {.dv = a[i] + ADD_CST}; - res[i] = t.s32v ^ XOR_CST; - } - memset(res + msize, 0, (res_size - msize) * sizeof(int32_t)); -} - -/** real centerlift mod 1, output in double space */ -EXPORT void tn32_to_dbl_ref(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -) { - static const uint32_t XOR_CST = (UINT32_C(1) << 31); - static const di_t OR_CST = {.dv = (double)(INT64_C(1) << (52 - 32))}; - static const double SUB_CST = 0.5 + (double)(INT64_C(1) << (52 - 32)); - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - uint32_t ai = a[i] ^ XOR_CST; - di_t t = {.u64v = OR_CST.u64v | (uint64_t)ai}; - res[i] = t.dv - SUB_CST; - } - memset(res + msize, 0, (res_size - msize) * sizeof(double)); -} - -/** round to the nearest int, output in i32 space */ -EXPORT void dbl_round_to_i32_ref(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - static const double ADD_CST = (double)((INT64_C(3) << (51)) + (INT64_C(1) << (31))); - static const int32_t XOR_CST = INT32_C(1) << 31; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - di_t t = {.dv = a[i] + ADD_CST}; - res[i] = t.s32v ^ XOR_CST; - } - memset(res + msize, 0, (res_size - msize) * sizeof(int32_t)); -} - -/** small int (int32 space) to double */ -EXPORT void i32_to_dbl_ref(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int32_t* a, uint64_t a_size // a -) { - static const uint32_t XOR_CST = (UINT32_C(1) << 31); - static const di_t OR_CST = {.dv = (double)(INT64_C(1) << 52)}; - static const double SUB_CST = (double)((INT64_C(1) << 52) + (INT64_C(1) << 31)); - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - uint32_t ai = a[i] ^ XOR_CST; - di_t t = {.u64v = OR_CST.u64v | (uint64_t)ai}; - res[i] = t.dv - SUB_CST; - } - memset(res + msize, 0, (res_size - msize) * sizeof(double)); -} - -/** round to the nearest int, output in int64 space */ -EXPORT void dbl_round_to_i64_ref(const MOD_Z* module, // - int64_t* res, uint64_t res_size, // res - const double* a, uint64_t a_size // a -) { - static const double ADD_CST = (double)(INT64_C(3) << (51)); - static const int64_t AND_CST = (INT64_C(1) << 52) - 1; - static const int64_t SUB_CST = INT64_C(1) << 51; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - di_t t = {.dv = a[i] + ADD_CST}; - res[i] = (t.s64v & AND_CST) - SUB_CST; - } - memset(res + msize, 0, (res_size - msize) * sizeof(int64_t)); -} - -/** small int (int64 space) to double */ -EXPORT void i64_to_dbl_ref(const MOD_Z* module, // - double* res, uint64_t res_size, // res - const int64_t* a, uint64_t a_size // a -) { - static const uint64_t ADD_CST = UINT64_C(1) << 51; - static const uint64_t AND_CST = (UINT64_C(1) << 52) - 1; - static const di_t OR_CST = {.dv = (INT64_C(1) << 52)}; - static const double SUB_CST = INT64_C(3) << 51; - const uint64_t msize = res_size < a_size ? res_size : a_size; - for (uint64_t i = 0; i < msize; ++i) { - di_t t = {.u64v = ((a[i] + ADD_CST) & AND_CST) | OR_CST.u64v}; - res[i] = t.dv - SUB_CST; - } - memset(res + msize, 0, (res_size - msize) * sizeof(double)); -}
@@ -1,4 +0,0 @@
-#define INTTYPE int16_t -#define INTSN i16 - -#include "zn_vmp_int32_avx.c"
@@ -1,4 +0,0 @@
-#define INTTYPE int16_t -#define INTSN i16 - -#include "zn_vmp_int32_ref.c"
@@ -1,223 +0,0 @@
-// This file is actually a template: it will be compiled multiple times with -// different INTTYPES -#ifndef INTTYPE -#define INTTYPE int32_t -#define INTSN i32 -#endif - -#include <immintrin.h> -#include <memory.h> - -#include "zn_arithmetic_private.h" - -#define concat_inner(aa, bb, cc) aa##_##bb##_##cc -#define concat(aa, bb, cc) concat_inner(aa, bb, cc) -#define zn32_vec_fn(cc) concat(zn32_vec, INTSN, cc) - -static void zn32_vec_mat32cols_avx_prefetch(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b) { - if (nrows == 0) { - memset(res, 0, 32 * sizeof(int32_t)); - return; - } - const int32_t* bb = b; - const int32_t* pref_bb = b; - const uint64_t pref_iters = 128; - const uint64_t pref_start = pref_iters < nrows ? pref_iters : nrows; - const uint64_t pref_last = pref_iters > nrows ? 0 : nrows - pref_iters; - // let's do some prefetching of the GSW key, since on some cpus, - // it helps - for (uint64_t i = 0; i < pref_start; ++i) { - __builtin_prefetch(pref_bb, 0, _MM_HINT_T0); - __builtin_prefetch(pref_bb + 16, 0, _MM_HINT_T0); - pref_bb += 32; - } - // we do the first iteration - __m256i x = _mm256_set1_epi32(a[0]); - __m256i r0 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb))); - __m256i r1 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8))); - __m256i r2 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16))); - __m256i r3 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 24))); - bb += 32; - uint64_t row = 1; - for (; // - row < pref_last; // - ++row, bb += 32) { - // prefetch the next iteration - __builtin_prefetch(pref_bb, 0, _MM_HINT_T0); - __builtin_prefetch(pref_bb + 16, 0, _MM_HINT_T0); - pref_bb += 32; - INTTYPE ai = a[row]; - if (ai == 0) continue; - x = _mm256_set1_epi32(ai); - r0 = _mm256_add_epi32(r0, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb)))); - r1 = _mm256_add_epi32(r1, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8)))); - r2 = _mm256_add_epi32(r2, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16)))); - r3 = _mm256_add_epi32(r3, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 24)))); - } - for (; // - row < nrows; // - ++row, bb += 32) { - INTTYPE ai = a[row]; - if (ai == 0) continue; - x = _mm256_set1_epi32(ai); - r0 = _mm256_add_epi32(r0, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb)))); - r1 = _mm256_add_epi32(r1, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8)))); - r2 = _mm256_add_epi32(r2, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16)))); - r3 = _mm256_add_epi32(r3, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 24)))); - } - _mm256_storeu_si256((__m256i*)(res), r0); - _mm256_storeu_si256((__m256i*)(res + 8), r1); - _mm256_storeu_si256((__m256i*)(res + 16), r2); - _mm256_storeu_si256((__m256i*)(res + 24), r3); -} - -void zn32_vec_fn(mat32cols_avx)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - if (nrows == 0) { - memset(res, 0, 32 * sizeof(int32_t)); - return; - } - const INTTYPE* aa = a; - const INTTYPE* const aaend = a + nrows; - const int32_t* bb = b; - __m256i x = _mm256_set1_epi32(*aa); - __m256i r0 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb))); - __m256i r1 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8))); - __m256i r2 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16))); - __m256i r3 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 24))); - bb += b_sl; - ++aa; - for (; // - aa < aaend; // - bb += b_sl, ++aa) { - INTTYPE ai = *aa; - if (ai == 0) continue; - x = _mm256_set1_epi32(ai); - r0 = _mm256_add_epi32(r0, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb)))); - r1 = _mm256_add_epi32(r1, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8)))); - r2 = _mm256_add_epi32(r2, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16)))); - r3 = _mm256_add_epi32(r3, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 24)))); - } - _mm256_storeu_si256((__m256i*)(res), r0); - _mm256_storeu_si256((__m256i*)(res + 8), r1); - _mm256_storeu_si256((__m256i*)(res + 16), r2); - _mm256_storeu_si256((__m256i*)(res + 24), r3); -} - -void zn32_vec_fn(mat24cols_avx)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - if (nrows == 0) { - memset(res, 0, 24 * sizeof(int32_t)); - return; - } - const INTTYPE* aa = a; - const INTTYPE* const aaend = a + nrows; - const int32_t* bb = b; - __m256i x = _mm256_set1_epi32(*aa); - __m256i r0 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb))); - __m256i r1 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8))); - __m256i r2 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16))); - bb += b_sl; - ++aa; - for (; // - aa < aaend; // - bb += b_sl, ++aa) { - INTTYPE ai = *aa; - if (ai == 0) continue; - x = _mm256_set1_epi32(ai); - r0 = _mm256_add_epi32(r0, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb)))); - r1 = _mm256_add_epi32(r1, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8)))); - r2 = _mm256_add_epi32(r2, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 16)))); - } - _mm256_storeu_si256((__m256i*)(res), r0); - _mm256_storeu_si256((__m256i*)(res + 8), r1); - _mm256_storeu_si256((__m256i*)(res + 16), r2); -} -void zn32_vec_fn(mat16cols_avx)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - if (nrows == 0) { - memset(res, 0, 16 * sizeof(int32_t)); - return; - } - const INTTYPE* aa = a; - const INTTYPE* const aaend = a + nrows; - const int32_t* bb = b; - __m256i x = _mm256_set1_epi32(*aa); - __m256i r0 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb))); - __m256i r1 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8))); - bb += b_sl; - ++aa; - for (; // - aa < aaend; // - bb += b_sl, ++aa) { - INTTYPE ai = *aa; - if (ai == 0) continue; - x = _mm256_set1_epi32(ai); - r0 = _mm256_add_epi32(r0, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb)))); - r1 = _mm256_add_epi32(r1, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb + 8)))); - } - _mm256_storeu_si256((__m256i*)(res), r0); - _mm256_storeu_si256((__m256i*)(res + 8), r1); -} - -void zn32_vec_fn(mat8cols_avx)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - if (nrows == 0) { - memset(res, 0, 8 * sizeof(int32_t)); - return; - } - const INTTYPE* aa = a; - const INTTYPE* const aaend = a + nrows; - const int32_t* bb = b; - __m256i x = _mm256_set1_epi32(*aa); - __m256i r0 = _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb))); - bb += b_sl; - ++aa; - for (; // - aa < aaend; // - bb += b_sl, ++aa) { - INTTYPE ai = *aa; - if (ai == 0) continue; - x = _mm256_set1_epi32(ai); - r0 = _mm256_add_epi32(r0, _mm256_mullo_epi32(x, _mm256_loadu_si256((__m256i*)(bb)))); - } - _mm256_storeu_si256((__m256i*)(res), r0); -} - -typedef void (*vm_f)(uint64_t nrows, // - int32_t* res, // - const INTTYPE* a, // - const int32_t* b, uint64_t b_sl // -); -static const vm_f zn32_vec_mat8kcols_avx[4] = { // - zn32_vec_fn(mat8cols_avx), // - zn32_vec_fn(mat16cols_avx), // - zn32_vec_fn(mat24cols_avx), // - zn32_vec_fn(mat32cols_avx)}; - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void concat(default_zn32_vmp_apply, INTSN, avx)( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // - const INTTYPE* a, uint64_t a_size, // - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols) { - const uint64_t rows = a_size < nrows ? a_size : nrows; - const uint64_t cols = res_size < ncols ? res_size : ncols; - const uint64_t ncolblk = cols >> 5; - const uint64_t ncolrem = cols & 31; - // copy the first full blocks - const uint64_t full_blk_size = nrows * 32; - const int32_t* mat = (int32_t*)pmat; - int32_t* rr = res; - for (uint64_t blk = 0; // - blk < ncolblk; // - ++blk, mat += full_blk_size, rr += 32) { - zn32_vec_mat32cols_avx_prefetch(rows, rr, a, mat); - } - // last block - if (ncolrem) { - uint64_t orig_rem = ncols - (ncolblk << 5); - uint64_t b_sl = orig_rem >= 32 ? 32 : orig_rem; - int32_t tmp[32]; - zn32_vec_mat8kcols_avx[(ncolrem - 1) >> 3](rows, tmp, a, mat, b_sl); - memcpy(rr, tmp, ncolrem * sizeof(int32_t)); - } - // trailing bytes - memset(res + cols, 0, (res_size - cols) * sizeof(int32_t)); -}
@@ -1,88 +0,0 @@
-// This file is actually a template: it will be compiled multiple times with -// different INTTYPES -#ifndef INTTYPE -#define INTTYPE int32_t -#define INTSN i32 -#endif - -#include <memory.h> - -#include "zn_arithmetic_private.h" - -#define concat_inner(aa, bb, cc) aa##_##bb##_##cc -#define concat(aa, bb, cc) concat_inner(aa, bb, cc) -#define zn32_vec_fn(cc) concat(zn32_vec, INTSN, cc) - -// the ref version shares the same implementation for each fixed column size -// optimized implementations may do something different. -static __always_inline void IMPL_zn32_vec_matcols_ref( - const uint64_t NCOLS, // fixed number of columns - uint64_t nrows, // nrows of b - int32_t* res, // result: size NCOLS, only the first min(b_sl, NCOLS) are relevant - const INTTYPE* a, // a: nrows-sized vector - const int32_t* b, uint64_t b_sl // b: nrows * min(b_sl, NCOLS) matrix -) { - memset(res, 0, NCOLS * sizeof(int32_t)); - for (uint64_t row = 0; row < nrows; ++row) { - int32_t ai = a[row]; - const int32_t* bb = b + row * b_sl; - for (uint64_t i = 0; i < NCOLS; ++i) { - res[i] += ai * bb[i]; - } - } -} - -void zn32_vec_fn(mat32cols_ref)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_matcols_ref(32, nrows, res, a, b, b_sl); -} -void zn32_vec_fn(mat24cols_ref)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_matcols_ref(24, nrows, res, a, b, b_sl); -} -void zn32_vec_fn(mat16cols_ref)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_matcols_ref(16, nrows, res, a, b, b_sl); -} -void zn32_vec_fn(mat8cols_ref)(uint64_t nrows, int32_t* res, const INTTYPE* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_matcols_ref(8, nrows, res, a, b, b_sl); -} - -typedef void (*vm_f)(uint64_t nrows, // - int32_t* res, // - const INTTYPE* a, // - const int32_t* b, uint64_t b_sl // -); -static const vm_f zn32_vec_mat8kcols_ref[4] = { // - zn32_vec_fn(mat8cols_ref), // - zn32_vec_fn(mat16cols_ref), // - zn32_vec_fn(mat24cols_ref), // - zn32_vec_fn(mat32cols_ref)}; - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void concat(default_zn32_vmp_apply, INTSN, ref)( // - const MOD_Z* module, // - int32_t* res, uint64_t res_size, // - const INTTYPE* a, uint64_t a_size, // - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols) { - const uint64_t rows = a_size < nrows ? a_size : nrows; - const uint64_t cols = res_size < ncols ? res_size : ncols; - const uint64_t ncolblk = cols >> 5; - const uint64_t ncolrem = cols & 31; - // copy the first full blocks - const uint32_t full_blk_size = nrows * 32; - const int32_t* mat = (int32_t*)pmat; - int32_t* rr = res; - for (uint64_t blk = 0; // - blk < ncolblk; // - ++blk, mat += full_blk_size, rr += 32) { - zn32_vec_fn(mat32cols_ref)(rows, rr, a, mat, 32); - } - // last block - if (ncolrem) { - uint64_t orig_rem = ncols - (ncolblk << 5); - uint64_t b_sl = orig_rem >= 32 ? 32 : orig_rem; - int32_t tmp[32]; - zn32_vec_mat8kcols_ref[(ncolrem - 1) >> 3](rows, tmp, a, mat, b_sl); - memcpy(rr, tmp, ncolrem * sizeof(int32_t)); - } - // trailing bytes - memset(res + cols, 0, (res_size - cols) * sizeof(int32_t)); -}
@@ -1,4 +0,0 @@
-#define INTTYPE int8_t -#define INTSN i8 - -#include "zn_vmp_int32_avx.c"
@@ -1,4 +0,0 @@
-#define INTTYPE int8_t -#define INTSN i8 - -#include "zn_vmp_int32_ref.c"
@@ -1,185 +0,0 @@
-#include <memory.h> - -#include "zn_arithmetic_private.h" - -/** @brief size in bytes of a prepared matrix (for custom allocation) */ -EXPORT uint64_t bytes_of_zn32_vmp_pmat(const MOD_Z* module, // N - uint64_t nrows, uint64_t ncols // dimensions -) { - return (nrows * ncols + 7) * sizeof(int32_t); -} - -/** @brief allocates a prepared matrix (release with delete_zn32_vmp_pmat) */ -EXPORT ZN32_VMP_PMAT* new_zn32_vmp_pmat(const MOD_Z* module, // N - uint64_t nrows, uint64_t ncols) { - return (ZN32_VMP_PMAT*)spqlios_alloc(bytes_of_zn32_vmp_pmat(module, nrows, ncols)); -} - -/** @brief deletes a prepared matrix (release with free) */ -EXPORT void delete_zn32_vmp_pmat(ZN32_VMP_PMAT* ptr) { spqlios_free(ptr); } - -/** @brief prepares a vmp matrix (contiguous row-major version) */ -EXPORT void default_zn32_vmp_prepare_contiguous_ref( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t* mat, uint64_t nrows, uint64_t ncols // a -) { - int32_t* const out = (int32_t*)pmat; - const uint64_t nblk = ncols >> 5; - const uint64_t ncols_rem = ncols & 31; - const uint64_t final_elems = (8 - nrows * ncols) & 7; - for (uint64_t blk = 0; blk < nblk; ++blk) { - int32_t* outblk = out + blk * nrows * 32; - const int32_t* srcblk = mat + blk * 32; - for (uint64_t row = 0; row < nrows; ++row) { - int32_t* dest = outblk + row * 32; - const int32_t* src = srcblk + row * ncols; - for (uint64_t i = 0; i < 32; ++i) { - dest[i] = src[i]; - } - } - } - // copy the last block if any - if (ncols_rem) { - int32_t* outblk = out + nblk * nrows * 32; - const int32_t* srcblk = mat + nblk * 32; - for (uint64_t row = 0; row < nrows; ++row) { - int32_t* dest = outblk + row * ncols_rem; - const int32_t* src = srcblk + row * ncols; - for (uint64_t i = 0; i < ncols_rem; ++i) { - dest[i] = src[i]; - } - } - } - // zero-out the final elements that may be accessed - if (final_elems) { - int32_t* f = out + nrows * ncols; - for (uint64_t i = 0; i < final_elems; ++i) { - f[i] = 0; - } - } -} - -/** @brief prepares a vmp matrix (mat[row]+col*N points to the item) */ -EXPORT void default_zn32_vmp_prepare_dblptr_ref( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t** mat, uint64_t nrows, uint64_t ncols // a -) { - for (uint64_t row_i = 0; row_i < nrows; ++row_i) { - default_zn32_vmp_prepare_row_ref(module, pmat, mat[row_i], row_i, nrows, ncols); - } -} - -/** @brief prepares the ith-row of a vmp matrix with nrows and ncols */ -EXPORT void default_zn32_vmp_prepare_row_ref( // - const MOD_Z* module, - ZN32_VMP_PMAT* pmat, // output - const int32_t* row, uint64_t row_i, uint64_t nrows, uint64_t ncols // a -) { - int32_t* const out = (int32_t*)pmat; - const uint64_t nblk = ncols >> 5; - const uint64_t ncols_rem = ncols & 31; - const uint64_t final_elems = (row_i == nrows - 1) && (8 - nrows * ncols) & 7; - for (uint64_t blk = 0; blk < nblk; ++blk) { - int32_t* outblk = out + blk * nrows * 32; - int32_t* dest = outblk + row_i * 32; - const int32_t* src = row + blk * 32; - for (uint64_t i = 0; i < 32; ++i) { - dest[i] = src[i]; - } - } - // copy the last block if any - if (ncols_rem) { - int32_t* outblk = out + nblk * nrows * 32; - int32_t* dest = outblk + row_i * ncols_rem; - const int32_t* src = row + nblk * 32; - for (uint64_t i = 0; i < ncols_rem; ++i) { - dest[i] = src[i]; - } - } - // zero-out the final elements that may be accessed - if (final_elems) { - int32_t* f = out + nrows * ncols; - for (uint64_t i = 0; i < final_elems; ++i) { - f[i] = 0; - } - } -} - -#if 0 - -#define IMPL_zn32_vec_ixxx_matyyycols_ref(NCOLS) \ - memset(res, 0, NCOLS * sizeof(int32_t)); \ - for (uint64_t row = 0; row < nrows; ++row) { \ - int32_t ai = a[row]; \ - const int32_t* bb = b + row * b_sl; \ - for (uint64_t i = 0; i < NCOLS; ++i) { \ - res[i] += ai * bb[i]; \ - } \ - } - -#define IMPL_zn32_vec_ixxx_mat8cols_ref() IMPL_zn32_vec_ixxx_matyyycols_ref(8) -#define IMPL_zn32_vec_ixxx_mat16cols_ref() IMPL_zn32_vec_ixxx_matyyycols_ref(16) -#define IMPL_zn32_vec_ixxx_mat24cols_ref() IMPL_zn32_vec_ixxx_matyyycols_ref(24) -#define IMPL_zn32_vec_ixxx_mat32cols_ref() IMPL_zn32_vec_ixxx_matyyycols_ref(32) - -void zn32_vec_i8_mat32cols_ref(uint64_t nrows, int32_t* res, const int8_t* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_ixxx_mat32cols_ref() -} -void zn32_vec_i16_mat32cols_ref(uint64_t nrows, int32_t* res, const int16_t* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_ixxx_mat32cols_ref() -} - -void zn32_vec_i32_mat32cols_ref(uint64_t nrows, int32_t* res, const int32_t* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_ixxx_mat32cols_ref() -} -void zn32_vec_i32_mat24cols_ref(uint64_t nrows, int32_t* res, const int32_t* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_ixxx_mat24cols_ref() -} -void zn32_vec_i32_mat16cols_ref(uint64_t nrows, int32_t* res, const int32_t* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_ixxx_mat16cols_ref() -} -void zn32_vec_i32_mat8cols_ref(uint64_t nrows, int32_t* res, const int32_t* a, const int32_t* b, uint64_t b_sl) { - IMPL_zn32_vec_ixxx_mat8cols_ref() -} -typedef void (*zn32_vec_i32_mat8kcols_ref_f)(uint64_t nrows, // - int32_t* res, // - const int32_t* a, // - const int32_t* b, uint64_t b_sl // -); -zn32_vec_i32_mat8kcols_ref_f zn32_vec_i32_mat8kcols_ref[4] = { // - zn32_vec_i32_mat8cols_ref, zn32_vec_i32_mat16cols_ref, // - zn32_vec_i32_mat24cols_ref, zn32_vec_i32_mat32cols_ref}; - -/** @brief applies a vmp product (int32_t* input) */ -EXPORT void default_zn32_vmp_apply_i32_ref(const MOD_Z* module, // - int32_t* res, uint64_t res_size, // - const int32_t* a, uint64_t a_size, // - const ZN32_VMP_PMAT* pmat, uint64_t nrows, uint64_t ncols) { - const uint64_t rows = a_size < nrows ? a_size : nrows; - const uint64_t cols = res_size < ncols ? res_size : ncols; - const uint64_t ncolblk = cols >> 5; - const uint64_t ncolrem = cols & 31; - // copy the first full blocks - const uint32_t full_blk_size = nrows * 32; - const int32_t* mat = (int32_t*)pmat; - int32_t* rr = res; - for (uint64_t blk = 0; // - blk < ncolblk; // - ++blk, mat += full_blk_size, rr += 32) { - zn32_vec_i32_mat32cols_ref(rows, rr, a, mat, 32); - } - // last block - if (ncolrem) { - uint64_t orig_rem = ncols - (ncolblk << 5); - uint64_t b_sl = orig_rem >= 32 ? 32 : orig_rem; - int32_t tmp[32]; - zn32_vec_i32_mat8kcols_ref[(ncolrem - 1) >> 3](rows, tmp, a, mat, b_sl); - memcpy(rr, tmp, ncolrem * sizeof(int32_t)); - } - // trailing bytes - memset(res + cols, 0, (res_size - cols) * sizeof(int32_t)); -} - -#endif
@@ -1,38 +0,0 @@
-#include "vec_znx_arithmetic_private.h" - -/** @brief res = a * b : small integer polynomial product */ -EXPORT void fft64_znx_small_single_product(const MODULE* module, // N - int64_t* res, // output - const int64_t* a, // a - const int64_t* b, // b - uint8_t* tmp) { - const uint64_t nn = module->nn; - double* const ffta = (double*)tmp; - double* const fftb = ((double*)tmp) + nn; - reim_from_znx64(module->mod.fft64.p_conv, ffta, a); - reim_from_znx64(module->mod.fft64.p_conv, fftb, b); - reim_fft(module->mod.fft64.p_fft, ffta); - reim_fft(module->mod.fft64.p_fft, fftb); - reim_fftvec_mul_simple(module->m, ffta, ffta, fftb); - reim_ifft(module->mod.fft64.p_ifft, ffta); - reim_to_znx64(module->mod.fft64.p_reim_to_znx, res, ffta); -} - -/** @brief tmp bytes required for znx_small_single_product */ -EXPORT uint64_t fft64_znx_small_single_product_tmp_bytes(const MODULE* module, uint64_t nn) { - return 2 * nn * sizeof(double); -} - -/** @brief res = a * b : small integer polynomial product */ -EXPORT void znx_small_single_product(const MODULE* module, // N - int64_t* res, // output - const int64_t* a, // a - const int64_t* b, // b - uint8_t* tmp) { - module->func.znx_small_single_product(module, res, a, b, tmp); -} - -/** @brief tmp bytes required for znx_small_single_product */ -EXPORT uint64_t znx_small_single_product_tmp_bytes(const MODULE* module, uint64_t nn) { - return module->func.znx_small_single_product_tmp_bytes(module, nn); -}
@@ -1,524 +0,0 @@
-#include "coeffs_arithmetic.h" - -#include <assert.h> -#include <memory.h> - -/** res = a + b */ -EXPORT void znx_add_i64_ref(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b) { - for (uint64_t i = 0; i < nn; ++i) { - res[i] = a[i] + b[i]; - } -} -/** res = a - b */ -EXPORT void znx_sub_i64_ref(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b) { - for (uint64_t i = 0; i < nn; ++i) { - res[i] = a[i] - b[i]; - } -} - -EXPORT void znx_negate_i64_ref(uint64_t nn, int64_t* res, const int64_t* a) { - for (uint64_t i = 0; i < nn; ++i) { - res[i] = -a[i]; - } -} -EXPORT void znx_copy_i64_ref(uint64_t nn, int64_t* res, const int64_t* a) { memcpy(res, a, nn * sizeof(int64_t)); } - -EXPORT void znx_zero_i64_ref(uint64_t nn, int64_t* res) { memset(res, 0, nn * sizeof(int64_t)); } - -EXPORT void rnx_divide_by_m_ref(uint64_t n, double m, double* res, const double* a) { - const double invm = 1. / m; - for (uint64_t i = 0; i < n; ++i) { - res[i] = a[i] * invm; - } -} - -EXPORT void rnx_rotate_f64(uint64_t nn, int64_t p, double* res, const double* in) { - uint64_t a = (-p) & (2 * nn - 1); // a= (-p) (pos)mod (2*nn) - - if (a < nn) { // rotate to the left - uint64_t nma = nn - a; - // rotate first half - for (uint64_t j = 0; j < nma; j++) { - res[j] = in[j + a]; - } - for (uint64_t j = nma; j < nn; j++) { - res[j] = -in[j - nma]; - } - } else { - a -= nn; - uint64_t nma = nn - a; - for (uint64_t j = 0; j < nma; j++) { - res[j] = -in[j + a]; - } - for (uint64_t j = nma; j < nn; j++) { - // rotate first half - res[j] = in[j - nma]; - } - } -} - -EXPORT void znx_rotate_i64(uint64_t nn, int64_t p, int64_t* res, const int64_t* in) { - uint64_t a = (-p) & (2 * nn - 1); // a= (-p) (pos)mod (2*nn) - - if (a < nn) { // rotate to the left - uint64_t nma = nn - a; - // rotate first half - for (uint64_t j = 0; j < nma; j++) { - res[j] = in[j + a]; - } - for (uint64_t j = nma; j < nn; j++) { - res[j] = -in[j - nma]; - } - } else { - a -= nn; - uint64_t nma = nn - a; - for (uint64_t j = 0; j < nma; j++) { - res[j] = -in[j + a]; - } - for (uint64_t j = nma; j < nn; j++) { - // rotate first half - res[j] = in[j - nma]; - } - } -} - -EXPORT void rnx_mul_xp_minus_one_f64(uint64_t nn, int64_t p, double* res, const double* in) { - uint64_t a = (-p) & (2 * nn - 1); // a= (-p) (pos)mod (2*nn) - if (a < nn) { // rotate to the left - uint64_t nma = nn - a; - // rotate first half - for (uint64_t j = 0; j < nma; j++) { - res[j] = in[j + a] - in[j]; - } - for (uint64_t j = nma; j < nn; j++) { - res[j] = -in[j - nma] - in[j]; - } - } else { - a -= nn; - uint64_t nma = nn - a; - for (uint64_t j = 0; j < nma; j++) { - res[j] = -in[j + a] - in[j]; - } - for (uint64_t j = nma; j < nn; j++) { - // rotate first half - res[j] = in[j - nma] - in[j]; - } - } -} - -EXPORT void znx_mul_xp_minus_one_i64(uint64_t nn, int64_t p, int64_t* res, const int64_t* in) { - uint64_t a = (-p) & (2 * nn - 1); // a= (-p) (pos)mod (2*nn) - if (a < nn) { // rotate to the left - uint64_t nma = nn - a; - // rotate first half - for (uint64_t j = 0; j < nma; j++) { - res[j] = in[j + a] - in[j]; - } - for (uint64_t j = nma; j < nn; j++) { - res[j] = -in[j - nma] - in[j]; - } - } else { - a -= nn; - uint64_t nma = nn - a; - for (uint64_t j = 0; j < nma; j++) { - res[j] = -in[j + a] - in[j]; - } - for (uint64_t j = nma; j < nn; j++) { - // rotate first half - res[j] = in[j - nma] - in[j]; - } - } -} - -EXPORT void znx_mul_xp_minus_one_inplace_i64(uint64_t nn, int64_t p, int64_t* res) { - const uint64_t _2mn = 2 * nn - 1; - const uint64_t _mn = nn - 1; - uint64_t nb_modif = 0; - uint64_t j_start = 0; - while (nb_modif < nn) { - // follow the cycle that start with j_start - uint64_t j = j_start; - int64_t tmp1 = res[j]; - do { - // find where the value should go, and with which sign - uint64_t new_j = (j + p) & _2mn; // mod 2n to get the position and sign - uint64_t new_j_n = new_j & _mn; // mod n to get just the position - // exchange this position with tmp1 (and take care of the sign) - int64_t tmp2 = res[new_j_n]; - res[new_j_n] = ((new_j < nn) ? tmp1 : -tmp1) - res[new_j_n]; - tmp1 = tmp2; - // move to the new location, and store the number of items modified - ++nb_modif; - j = new_j_n; - } while (j != j_start); - // move to the start of the next cycle: - // we need to find an index that has not been touched yet, and pick it as next j_start. - // in practice, it is enough to do +1, because the group of rotations is cyclic and 1 is a generator. - ++j_start; - } -} - -// 0 < p < 2nn -EXPORT void rnx_automorphism_f64(uint64_t nn, int64_t p, double* res, const double* in) { - res[0] = in[0]; - uint64_t a = 0; - uint64_t _2mn = 2 * nn - 1; - for (uint64_t i = 1; i < nn; i++) { - a = (a + p) & _2mn; // i*p mod 2n - if (a < nn) { - res[a] = in[i]; // res[ip mod 2n] = res[i] - } else { - res[a - nn] = -in[i]; - } - } -} - -EXPORT void znx_automorphism_i64(uint64_t nn, int64_t p, int64_t* res, const int64_t* in) { - res[0] = in[0]; - uint64_t a = 0; - uint64_t _2mn = 2 * nn - 1; - for (uint64_t i = 1; i < nn; i++) { - a = (a + p) & _2mn; - if (a < nn) { - res[a] = in[i]; // res[ip mod 2n] = res[i] - } else { - res[a - nn] = -in[i]; - } - } -} - -EXPORT void rnx_rotate_inplace_f64(uint64_t nn, int64_t p, double* res) { - const uint64_t _2mn = 2 * nn - 1; - const uint64_t _mn = nn - 1; - uint64_t nb_modif = 0; - uint64_t j_start = 0; - while (nb_modif < nn) { - // follow the cycle that start with j_start - uint64_t j = j_start; - double tmp1 = res[j]; - do { - // find where the value should go, and with which sign - uint64_t new_j = (j + p) & _2mn; // mod 2n to get the position and sign - uint64_t new_j_n = new_j & _mn; // mod n to get just the position - // exchange this position with tmp1 (and take care of the sign) - double tmp2 = res[new_j_n]; - res[new_j_n] = (new_j < nn) ? tmp1 : -tmp1; - tmp1 = tmp2; - // move to the new location, and store the number of items modified - ++nb_modif; - j = new_j_n; - } while (j != j_start); - // move to the start of the next cycle: - // we need to find an index that has not been touched yet, and pick it as next j_start. - // in practice, it is enough to do +1, because the group of rotations is cyclic and 1 is a generator. - ++j_start; - } -} - -EXPORT void znx_rotate_inplace_i64(uint64_t nn, int64_t p, int64_t* res) { - const uint64_t _2mn = 2 * nn - 1; - const uint64_t _mn = nn - 1; - uint64_t nb_modif = 0; - uint64_t j_start = 0; - while (nb_modif < nn) { - // follow the cycle that start with j_start - uint64_t j = j_start; - int64_t tmp1 = res[j]; - do { - // find where the value should go, and with which sign - uint64_t new_j = (j + p) & _2mn; // mod 2n to get the position and sign - uint64_t new_j_n = new_j & _mn; // mod n to get just the position - // exchange this position with tmp1 (and take care of the sign) - int64_t tmp2 = res[new_j_n]; - res[new_j_n] = (new_j < nn) ? tmp1 : -tmp1; - tmp1 = tmp2; - // move to the new location, and store the number of items modified - ++nb_modif; - j = new_j_n; - } while (j != j_start); - // move to the start of the next cycle: - // we need to find an index that has not been touched yet, and pick it as next j_start. - // in practice, it is enough to do +1, because the group of rotations is cyclic and 1 is a generator. - ++j_start; - } -} - -EXPORT void rnx_mul_xp_minus_one_inplace_f64(uint64_t nn, int64_t p, double* res) { - const uint64_t _2mn = 2 * nn - 1; - const uint64_t _mn = nn - 1; - uint64_t nb_modif = 0; - uint64_t j_start = 0; - while (nb_modif < nn) { - // follow the cycle that start with j_start - uint64_t j = j_start; - double tmp1 = res[j]; - do { - // find where the value should go, and with which sign - uint64_t new_j = (j + p) & _2mn; // mod 2n to get the position and sign - uint64_t new_j_n = new_j & _mn; // mod n to get just the position - // exchange this position with tmp1 (and take care of the sign) - double tmp2 = res[new_j_n]; - res[new_j_n] = ((new_j < nn) ? tmp1 : -tmp1) - res[new_j_n]; - tmp1 = tmp2; - // move to the new location, and store the number of items modified - ++nb_modif; - j = new_j_n; - } while (j != j_start); - // move to the start of the next cycle: - // we need to find an index that has not been touched yet, and pick it as next j_start. - // in practice, it is enough to do +1, because the group of rotations is cyclic and 1 is a generator. - ++j_start; - } -} - -__always_inline int64_t get_base_k_digit(const int64_t x, const uint64_t base_k) { - return (x << (64 - base_k)) >> (64 - base_k); -} - -__always_inline int64_t get_base_k_carry(const int64_t x, const int64_t digit, const uint64_t base_k) { - return (x - digit) >> base_k; -} - -EXPORT void znx_normalize(uint64_t nn, uint64_t base_k, int64_t* out, int64_t* carry_out, const int64_t* in, - const int64_t* carry_in) { - assert(in); - if (out != 0) { - if (carry_in != 0x0 && carry_out != 0x0) { - // with carry in and carry out is computed - for (uint64_t i = 0; i < nn; ++i) { - const int64_t x = in[i]; - const int64_t cin = carry_in[i]; - - int64_t digit = get_base_k_digit(x, base_k); - int64_t carry = get_base_k_carry(x, digit, base_k); - int64_t digit_plus_cin = digit + cin; - int64_t y = get_base_k_digit(digit_plus_cin, base_k); - int64_t cout = carry + get_base_k_carry(digit_plus_cin, y, base_k); - - out[i] = y; - carry_out[i] = cout; - } - } else if (carry_in != 0) { - // with carry in and carry out is dropped - for (uint64_t i = 0; i < nn; ++i) { - const int64_t x = in[i]; - const int64_t cin = carry_in[i]; - - int64_t digit = get_base_k_digit(x, base_k); - int64_t digit_plus_cin = digit + cin; - int64_t y = get_base_k_digit(digit_plus_cin, base_k); - - out[i] = y; - } - - } else if (carry_out != 0) { - // no carry in and carry out is computed - for (uint64_t i = 0; i < nn; ++i) { - const int64_t x = in[i]; - - int64_t y = get_base_k_digit(x, base_k); - int64_t cout = get_base_k_carry(x, y, base_k); - - out[i] = y; - carry_out[i] = cout; - } - - } else { - // no carry in and carry out is dropped - for (uint64_t i = 0; i < nn; ++i) { - out[i] = get_base_k_digit(in[i], base_k); - } - } - } else { - assert(carry_out); - if (carry_in != 0x0) { - // with carry in and carry out is computed - for (uint64_t i = 0; i < nn; ++i) { - const int64_t x = in[i]; - const int64_t cin = carry_in[i]; - - int64_t digit = get_base_k_digit(x, base_k); - int64_t carry = get_base_k_carry(x, digit, base_k); - int64_t digit_plus_cin = digit + cin; - int64_t y = get_base_k_digit(digit_plus_cin, base_k); - int64_t cout = carry + get_base_k_carry(digit_plus_cin, y, base_k); - - carry_out[i] = cout; - } - } else { - // no carry in and carry out is computed - for (uint64_t i = 0; i < nn; ++i) { - const int64_t x = in[i]; - - int64_t y = get_base_k_digit(x, base_k); - int64_t cout = get_base_k_carry(x, y, base_k); - - carry_out[i] = cout; - } - } - } -} - -void znx_automorphism_inplace_i64(uint64_t nn, int64_t p, int64_t* res) { - const uint64_t _2mn = 2 * nn - 1; - const uint64_t _mn = nn - 1; - const uint64_t m = nn >> 1; - // reduce p mod 2n - p &= _2mn; - // uint64_t vp = p & _2mn; - /// uint64_t target_modifs = m >> 1; - // we proceed by increasing binary valuation - for (uint64_t binval = 1, vp = p & _2mn, orb_size = m; binval < nn; - binval <<= 1, vp = (vp << 1) & _2mn, orb_size >>= 1) { - // In this loop, we are going to treat the orbit of indexes = binval mod 2.binval. - // At the beginning of this loop we have: - // vp = binval * p mod 2n - // target_modif = m / binval (i.e. order of the orbit binval % 2.binval) - - // first, handle the orders 1 and 2. - // if p*binval == binval % 2n: we're done! - if (vp == binval) return; - // if p*binval == -binval % 2n: nega-mirror the orbit and all the sub-orbits and exit! - if (((vp + binval) & _2mn) == 0) { - for (uint64_t j = binval; j < m; j += binval) { - int64_t tmp = res[j]; - res[j] = -res[nn - j]; - res[nn - j] = -tmp; - } - res[m] = -res[m]; - return; - } - // if p*binval == binval + n % 2n: negate the orbit and exit - if (((vp - binval) & _mn) == 0) { - for (uint64_t j = binval; j < nn; j += 2 * binval) { - res[j] = -res[j]; - } - return; - } - // if p*binval == n - binval % 2n: mirror the orbit and continue! - if (((vp + binval) & _mn) == 0) { - for (uint64_t j = binval; j < m; j += 2 * binval) { - int64_t tmp = res[j]; - res[j] = res[nn - j]; - res[nn - j] = tmp; - } - continue; - } - // otherwise we will follow the orbit cycles, - // starting from binval and -binval in parallel - uint64_t j_start = binval; - uint64_t nb_modif = 0; - while (nb_modif < orb_size) { - // follow the cycle that start with j_start - uint64_t j = j_start; - int64_t tmp1 = res[j]; - int64_t tmp2 = res[nn - j]; - do { - // find where the value should go, and with which sign - uint64_t new_j = (j * p) & _2mn; // mod 2n to get the position and sign - uint64_t new_j_n = new_j & _mn; // mod n to get just the position - // exchange this position with tmp1 (and take care of the sign) - int64_t tmp1a = res[new_j_n]; - int64_t tmp2a = res[nn - new_j_n]; - if (new_j < nn) { - res[new_j_n] = tmp1; - res[nn - new_j_n] = tmp2; - } else { - res[new_j_n] = -tmp1; - res[nn - new_j_n] = -tmp2; - } - tmp1 = tmp1a; - tmp2 = tmp2a; - // move to the new location, and store the number of items modified - nb_modif += 2; - j = new_j_n; - } while (j != j_start); - // move to the start of the next cycle: - // we need to find an index that has not been touched yet, and pick it as next j_start. - // in practice, it is enough to do *5, because 5 is a generator. - j_start = (5 * j_start) & _mn; - } - } -} - -void rnx_automorphism_inplace_f64(uint64_t nn, int64_t p, double* res) { - const uint64_t _2mn = 2 * nn - 1; - const uint64_t _mn = nn - 1; - const uint64_t m = nn >> 1; - // reduce p mod 2n - p &= _2mn; - // uint64_t vp = p & _2mn; - /// uint64_t target_modifs = m >> 1; - // we proceed by increasing binary valuation - for (uint64_t binval = 1, vp = p & _2mn, orb_size = m; binval < nn; - binval <<= 1, vp = (vp << 1) & _2mn, orb_size >>= 1) { - // In this loop, we are going to treat the orbit of indexes = binval mod 2.binval. - // At the beginning of this loop we have: - // vp = binval * p mod 2n - // target_modif = m / binval (i.e. order of the orbit binval % 2.binval) - - // first, handle the orders 1 and 2. - // if p*binval == binval % 2n: we're done! - if (vp == binval) return; - // if p*binval == -binval % 2n: nega-mirror the orbit and all the sub-orbits and exit! - if (((vp + binval) & _2mn) == 0) { - for (uint64_t j = binval; j < m; j += binval) { - double tmp = res[j]; - res[j] = -res[nn - j]; - res[nn - j] = -tmp; - } - res[m] = -res[m]; - return; - } - // if p*binval == binval + n % 2n: negate the orbit and exit - if (((vp - binval) & _mn) == 0) { - for (uint64_t j = binval; j < nn; j += 2 * binval) { - res[j] = -res[j]; - } - return; - } - // if p*binval == n - binval % 2n: mirror the orbit and continue! - if (((vp + binval) & _mn) == 0) { - for (uint64_t j = binval; j < m; j += 2 * binval) { - double tmp = res[j]; - res[j] = res[nn - j]; - res[nn - j] = tmp; - } - continue; - } - // otherwise we will follow the orbit cycles, - // starting from binval and -binval in parallel - uint64_t j_start = binval; - uint64_t nb_modif = 0; - while (nb_modif < orb_size) { - // follow the cycle that start with j_start - uint64_t j = j_start; - double tmp1 = res[j]; - double tmp2 = res[nn - j]; - do { - // find where the value should go, and with which sign - uint64_t new_j = (j * p) & _2mn; // mod 2n to get the position and sign - uint64_t new_j_n = new_j & _mn; // mod n to get just the position - // exchange this position with tmp1 (and take care of the sign) - double tmp1a = res[new_j_n]; - double tmp2a = res[nn - new_j_n]; - if (new_j < nn) { - res[new_j_n] = tmp1; - res[nn - new_j_n] = tmp2; - } else { - res[new_j_n] = -tmp1; - res[nn - new_j_n] = -tmp2; - } - tmp1 = tmp1a; - tmp2 = tmp2a; - // move to the new location, and store the number of items modified - nb_modif += 2; - j = new_j_n; - } while (j != j_start); - // move to the start of the next cycle: - // we need to find an index that has not been touched yet, and pick it as next j_start. - // in practice, it is enough to do *5, because 5 is a generator. - j_start = (5 * j_start) & _mn; - } - } -}
@@ -1,79 +0,0 @@
-#ifndef SPQLIOS_COEFFS_ARITHMETIC_H -#define SPQLIOS_COEFFS_ARITHMETIC_H - -#include "../commons.h" - -/** res = a + b */ -EXPORT void znx_add_i64_ref(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b); -EXPORT void znx_add_i64_avx(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b); -/** res = a - b */ -EXPORT void znx_sub_i64_ref(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b); -EXPORT void znx_sub_i64_avx(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b); -/** res = -a */ -EXPORT void znx_negate_i64_ref(uint64_t nn, int64_t* res, const int64_t* a); -EXPORT void znx_negate_i64_avx(uint64_t nn, int64_t* res, const int64_t* a); -/** res = a */ -EXPORT void znx_copy_i64_ref(uint64_t nn, int64_t* res, const int64_t* a); -/** res = 0 */ -EXPORT void znx_zero_i64_ref(uint64_t nn, int64_t* res); - -/** res = a / m where m is a power of 2 */ -EXPORT void rnx_divide_by_m_ref(uint64_t nn, double m, double* res, const double* a); -EXPORT void rnx_divide_by_m_avx(uint64_t nn, double m, double* res, const double* a); - -/** - * @param res = X^p *in mod X^nn +1 - * @param nn the ring dimension - * @param p a power for the rotation -2nn <= p <= 2nn - * @param in is a rnx/znx vector of dimension nn - */ -EXPORT void rnx_rotate_f64(uint64_t nn, int64_t p, double* res, const double* in); -EXPORT void znx_rotate_i64(uint64_t nn, int64_t p, int64_t* res, const int64_t* in); -EXPORT void rnx_rotate_inplace_f64(uint64_t nn, int64_t p, double* res); -EXPORT void znx_rotate_inplace_i64(uint64_t nn, int64_t p, int64_t* res); - -/** - * @brief res(X) = in(X^p) - * @param nn the ring dimension - * @param p is odd integer and must be between 0 < p < 2nn - * @param in is a rnx/znx vector of dimension nn - */ -EXPORT void rnx_automorphism_f64(uint64_t nn, int64_t p, double* res, const double* in); -EXPORT void znx_automorphism_i64(uint64_t nn, int64_t p, int64_t* res, const int64_t* in); -EXPORT void rnx_automorphism_inplace_f64(uint64_t nn, int64_t p, double* res); -EXPORT void znx_automorphism_inplace_i64(uint64_t nn, int64_t p, int64_t* res); - -/** - * @brief res = (X^p-1).in - * @param nn the ring dimension - * @param p must be between -2nn <= p <= 2nn - * @param in is a rnx/znx vector of dimension nn - */ -EXPORT void rnx_mul_xp_minus_one_f64(uint64_t nn, int64_t p, double* res, const double* in); -EXPORT void znx_mul_xp_minus_one_i64(uint64_t nn, int64_t p, int64_t* res, const int64_t* in); -EXPORT void rnx_mul_xp_minus_one_inplace_f64(uint64_t nn, int64_t p, double* res); -EXPORT void znx_mul_xp_minus_one_inplace_i64(uint64_t nn, int64_t p, int64_t* res); - -/** - * @brief Normalize input plus carry mod-2^k. The following - * equality holds @c {in + carry_in == out + carry_out . 2^k}. - * - * @c in must be in [-2^62 .. 2^62] - * - * @c out is in [ -2^(base_k-1), 2^(base_k-1) [. - * - * @c carry_in and @carry_out have at most 64+1-k bits. - * - * Null @c carry_in or @c carry_out are ignored. - * - * @param[in] nn the ring dimension - * @param[in] base_k the base k - * @param out output normalized znx - * @param carry_out output carry znx - * @param[in] in input znx - * @param[in] carry_in input carry znx - */ -EXPORT void znx_normalize(uint64_t nn, uint64_t base_k, int64_t* out, int64_t* carry_out, const int64_t* in, - const int64_t* carry_in); - -#endif // SPQLIOS_COEFFS_ARITHMETIC_H
@@ -1,124 +0,0 @@
-#include <immintrin.h> - -#include "../commons_private.h" -#include "coeffs_arithmetic.h" - -// res = a + b. dimension n must be a power of 2 -EXPORT void znx_add_i64_avx(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b) { - if (nn <= 2) { - if (nn == 1) { - res[0] = a[0] + b[0]; - } else { - _mm_storeu_si128((__m128i*)res, // - _mm_add_epi64( // - _mm_loadu_si128((__m128i*)a), // - _mm_loadu_si128((__m128i*)b))); - } - } else { - const __m256i* aa = (__m256i*)a; - const __m256i* bb = (__m256i*)b; - __m256i* rr = (__m256i*)res; - __m256i* const rrend = (__m256i*)(res + nn); - do { - _mm256_storeu_si256(rr, // - _mm256_add_epi64( // - _mm256_loadu_si256(aa), // - _mm256_loadu_si256(bb))); - ++rr; - ++aa; - ++bb; - } while (rr < rrend); - } -} - -// res = a - b. dimension n must be a power of 2 -EXPORT void znx_sub_i64_avx(uint64_t nn, int64_t* res, const int64_t* a, const int64_t* b) { - if (nn <= 2) { - if (nn == 1) { - res[0] = a[0] - b[0]; - } else { - _mm_storeu_si128((__m128i*)res, // - _mm_sub_epi64( // - _mm_loadu_si128((__m128i*)a), // - _mm_loadu_si128((__m128i*)b))); - } - } else { - const __m256i* aa = (__m256i*)a; - const __m256i* bb = (__m256i*)b; - __m256i* rr = (__m256i*)res; - __m256i* const rrend = (__m256i*)(res + nn); - do { - _mm256_storeu_si256(rr, // - _mm256_sub_epi64( // - _mm256_loadu_si256(aa), // - _mm256_loadu_si256(bb))); - ++rr; - ++aa; - ++bb; - } while (rr < rrend); - } -} - -EXPORT void znx_negate_i64_avx(uint64_t nn, int64_t* res, const int64_t* a) { - if (nn <= 2) { - if (nn == 1) { - res[0] = -a[0]; - } else { - _mm_storeu_si128((__m128i*)res, // - _mm_sub_epi64( // - _mm_set1_epi64x(0), // - _mm_loadu_si128((__m128i*)a))); - } - } else { - const __m256i* aa = (__m256i*)a; - __m256i* rr = (__m256i*)res; - __m256i* const rrend = (__m256i*)(res + nn); - do { - _mm256_storeu_si256(rr, // - _mm256_sub_epi64( // - _mm256_set1_epi64x(0), // - _mm256_loadu_si256(aa))); - ++rr; - ++aa; - } while (rr < rrend); - } -} - -EXPORT void rnx_divide_by_m_avx(uint64_t n, double m, double* res, const double* a) { - // TODO: see if there is a faster way of dividing by a power of 2? - const double invm = 1. / m; - if (n < 8) { - switch (n) { - case 1: - *res = *a * invm; - break; - case 2: - _mm_storeu_pd(res, // - _mm_mul_pd(_mm_loadu_pd(a), // - _mm_set1_pd(invm))); - break; - case 4: - _mm256_storeu_pd(res, // - _mm256_mul_pd(_mm256_loadu_pd(a), // - _mm256_set1_pd(invm))); - break; - default: - NOT_SUPPORTED(); // non-power of 2 - } - return; - } - const __m256d invm256 = _mm256_set1_pd(invm); - double* rr = res; - const double* aa = a; - const double* const aaend = a + n; - do { - _mm256_storeu_pd(rr, // - _mm256_mul_pd(_mm256_loadu_pd(aa), // - invm256)); - _mm256_storeu_pd(rr + 4, // - _mm256_mul_pd(_mm256_loadu_pd(aa + 4), // - invm256)); - rr += 8; - aa += 8; - } while (aa < aaend); -}
@@ -1,165 +0,0 @@
-#include "commons.h" - -#include <math.h> -#include <stdio.h> -#include <stdlib.h> - -EXPORT void* UNDEFINED_p_ii(int32_t n, int32_t m) { UNDEFINED(); } -EXPORT void* UNDEFINED_p_uu(uint32_t n, uint32_t m) { UNDEFINED(); } -EXPORT double* UNDEFINED_dp_pi(const void* p, int32_t n) { UNDEFINED(); } -EXPORT void* UNDEFINED_vp_pi(const void* p, int32_t n) { UNDEFINED(); } -EXPORT void* UNDEFINED_vp_pu(const void* p, uint32_t n) { UNDEFINED(); } -EXPORT void UNDEFINED_v_vpdp(const void* p, double* a) { UNDEFINED(); } -EXPORT void UNDEFINED_v_vpvp(const void* p, void* a) { UNDEFINED(); } -EXPORT double* NOT_IMPLEMENTED_dp_i(int32_t n) { NOT_IMPLEMENTED(); } -EXPORT void* NOT_IMPLEMENTED_vp_i(int32_t n) { NOT_IMPLEMENTED(); } -EXPORT void* NOT_IMPLEMENTED_vp_u(uint32_t n) { NOT_IMPLEMENTED(); } -EXPORT void NOT_IMPLEMENTED_v_dp(double* a) { NOT_IMPLEMENTED(); } -EXPORT void NOT_IMPLEMENTED_v_vp(void* p) { NOT_IMPLEMENTED(); } -EXPORT void NOT_IMPLEMENTED_v_idpdpdp(int32_t n, double* a, const double* b, const double* c) { NOT_IMPLEMENTED(); } -EXPORT void NOT_IMPLEMENTED_v_uvpcvpcvp(uint32_t n, void* r, const void* a, const void* b) { NOT_IMPLEMENTED(); } -EXPORT void NOT_IMPLEMENTED_v_uvpvpcvp(uint32_t n, void* a, void* b, const void* o) { NOT_IMPLEMENTED(); } - -#ifdef _WIN32 -#define __always_inline inline __attribute((always_inline)) -#endif - -void internal_accurate_sincos(double* rcos, double* rsin, double x) { - double _4_x_over_pi = 4 * x / M_PI; - int64_t int_part = ((int64_t)rint(_4_x_over_pi)) & 7; - double frac_part = _4_x_over_pi - (double)(int_part); - double frac_x = M_PI * frac_part / 4.; - // compute the taylor series - double cosp = 1.; - double sinp = 0.; - double powx = 1.; - int64_t nn = 0; - while (fabs(powx) > 1e-20) { - ++nn; - powx = powx * frac_x / (double)(nn); // x^n/n! - switch (nn & 3) { - case 0: - cosp += powx; - break; - case 1: - sinp += powx; - break; - case 2: - cosp -= powx; - break; - case 3: - sinp -= powx; - break; - default: - abort(); // impossible - } - } - // final multiplication - switch (int_part) { - case 0: - *rcos = cosp; - *rsin = sinp; - break; - case 1: - *rcos = M_SQRT1_2 * (cosp - sinp); - *rsin = M_SQRT1_2 * (cosp + sinp); - break; - case 2: - *rcos = -sinp; - *rsin = cosp; - break; - case 3: - *rcos = -M_SQRT1_2 * (cosp + sinp); - *rsin = M_SQRT1_2 * (cosp - sinp); - break; - case 4: - *rcos = -cosp; - *rsin = -sinp; - break; - case 5: - *rcos = -M_SQRT1_2 * (cosp - sinp); - *rsin = -M_SQRT1_2 * (cosp + sinp); - break; - case 6: - *rcos = sinp; - *rsin = -cosp; - break; - case 7: - *rcos = M_SQRT1_2 * (cosp + sinp); - *rsin = -M_SQRT1_2 * (cosp - sinp); - break; - default: - abort(); // impossible - } - if (fabs(cos(x) - *rcos) > 1e-10 || fabs(sin(x) - *rsin) > 1e-10) { - printf("cos(%.17lf) =? %.17lf instead of %.17lf\n", x, *rcos, cos(x)); - printf("sin(%.17lf) =? %.17lf instead of %.17lf\n", x, *rsin, sin(x)); - printf("fracx = %.17lf\n", frac_x); - printf("cosp = %.17lf\n", cosp); - printf("sinp = %.17lf\n", sinp); - printf("nn = %d\n", (int)(nn)); - } -} - -double internal_accurate_cos(double x) { - double rcos, rsin; - internal_accurate_sincos(&rcos, &rsin, x); - return rcos; -} -double internal_accurate_sin(double x) { - double rcos, rsin; - internal_accurate_sincos(&rcos, &rsin, x); - return rsin; -} - -EXPORT void spqlios_debug_free(void* addr) { free((uint8_t*)addr - 64); } - -EXPORT void* spqlios_debug_alloc(uint64_t size) { return (uint8_t*)malloc(size + 64) + 64; } - -EXPORT void spqlios_free(void* addr) { -#ifndef NDEBUG - // in debug mode, we deallocated with spqlios_debug_free() - spqlios_debug_free(addr); -#else - // in release mode, the function will free aligned memory -#ifdef _WIN32 - _aligned_free(addr); -#else - free(addr); -#endif -#endif -} - -EXPORT void* spqlios_alloc(uint64_t size) { -#ifndef NDEBUG - // in debug mode, the function will not necessarily have any particular alignment - // it will also ensure that memory can only be deallocated with spqlios_free() - return spqlios_debug_alloc(size); -#else - // in release mode, the function will return 64-bytes aligned memory -#ifdef _WIN32 - void* reps = _aligned_malloc((size + 63) & (UINT64_C(-64)), 64); -#else - void* reps = aligned_alloc(64, (size + 63) & (UINT64_C(-64))); -#endif - if (reps == 0) FATAL_ERROR("Out of memory"); - return reps; -#endif -} - -EXPORT void* spqlios_alloc_custom_align(uint64_t align, uint64_t size) { -#ifndef NDEBUG - // in debug mode, the function will not necessarily have any particular alignment - // it will also ensure that memory can only be deallocated with spqlios_free() - return spqlios_debug_alloc(size); -#else - // in release mode, the function will return aligned memory -#ifdef _WIN32 - void* reps = _aligned_malloc(size, align); -#else - void* reps = aligned_alloc(align, size); -#endif - if (reps == 0) FATAL_ERROR("Out of memory"); - return reps; -#endif -}
@@ -1,77 +0,0 @@
-#ifndef SPQLIOS_COMMONS_H -#define SPQLIOS_COMMONS_H - -#ifdef __cplusplus -#include <cstdint> -#include <cstdio> -#include <cstdlib> -#define EXPORT extern "C" -#define EXPORT_DECL extern "C" -#else -#include <stdint.h> -#include <stdio.h> -#include <stdlib.h> -#define EXPORT -#define EXPORT_DECL extern -#define nullptr 0x0; -#endif - -#define UNDEFINED() \ - { \ - fprintf(stderr, "UNDEFINED!!!\n"); \ - abort(); \ - } -#define NOT_IMPLEMENTED() \ - { \ - fprintf(stderr, "NOT IMPLEMENTED!!!\n"); \ - abort(); \ - } -#define FATAL_ERROR(MESSAGE) \ - { \ - fprintf(stderr, "ERROR: %s\n", (MESSAGE)); \ - abort(); \ - } - -EXPORT void* UNDEFINED_p_ii(int32_t n, int32_t m); -EXPORT void* UNDEFINED_p_uu(uint32_t n, uint32_t m); -EXPORT double* UNDEFINED_dp_pi(const void* p, int32_t n); -EXPORT void* UNDEFINED_vp_pi(const void* p, int32_t n); -EXPORT void* UNDEFINED_vp_pu(const void* p, uint32_t n); -EXPORT void UNDEFINED_v_vpdp(const void* p, double* a); -EXPORT void UNDEFINED_v_vpvp(const void* p, void* a); -EXPORT double* NOT_IMPLEMENTED_dp_i(int32_t n); -EXPORT void* NOT_IMPLEMENTED_vp_i(int32_t n); -EXPORT void* NOT_IMPLEMENTED_vp_u(uint32_t n); -EXPORT void NOT_IMPLEMENTED_v_dp(double* a); -EXPORT void NOT_IMPLEMENTED_v_vp(void* p); -EXPORT void NOT_IMPLEMENTED_v_idpdpdp(int32_t n, double* a, const double* b, const double* c); -EXPORT void NOT_IMPLEMENTED_v_uvpcvpcvp(uint32_t n, void* r, const void* a, const void* b); -EXPORT void NOT_IMPLEMENTED_v_uvpvpcvp(uint32_t n, void* a, void* b, const void* o); - -// windows - -#if defined(_WIN32) || defined(__APPLE__) -#define __always_inline inline __attribute((always_inline)) -#endif - -EXPORT void spqlios_free(void* address); - -EXPORT void* spqlios_alloc(uint64_t size); -EXPORT void* spqlios_alloc_custom_align(uint64_t align, uint64_t size); - -#define USE_LIBM_SIN_COS -#ifndef USE_LIBM_SIN_COS -// if at some point, we want to remove the libm dependency, we can -// consider this: -EXPORT double internal_accurate_cos(double x); -EXPORT double internal_accurate_sin(double x); -EXPORT void internal_accurate_sincos(double* rcos, double* rsin, double x); -#define m_accurate_cos internal_accurate_cos -#define m_accurate_sin internal_accurate_sin -#else -// let's use libm sin and cos -#define m_accurate_cos cos -#define m_accurate_sin sin -#endif - -#endif // SPQLIOS_COMMONS_H
@@ -1,55 +0,0 @@
-#include "commons_private.h" - -#include <stdio.h> -#include <stdlib.h> - -#include "commons.h" - -EXPORT void* spqlios_error(const char* error) { - fputs(error, stderr); - abort(); - return nullptr; -} -EXPORT void* spqlios_keep_or_free(void* ptr, void* ptr2) { - if (!ptr2) { - free(ptr); - } - return ptr2; -} - -EXPORT uint32_t log2m(uint32_t m) { - uint32_t a = m - 1; - if (m & a) FATAL_ERROR("m must be a power of two"); - a = (a & 0x55555555u) + ((a >> 1) & 0x55555555u); - a = (a & 0x33333333u) + ((a >> 2) & 0x33333333u); - a = (a & 0x0F0F0F0Fu) + ((a >> 4) & 0x0F0F0F0Fu); - a = (a & 0x00FF00FFu) + ((a >> 8) & 0x00FF00FFu); - return (a & 0x0000FFFFu) + ((a >> 16) & 0x0000FFFFu); -} - -EXPORT uint64_t is_not_pow2_double(void* doublevalue) { return (*(uint64_t*)doublevalue) & 0x7FFFFFFFFFFFFUL; } - -uint32_t revbits(uint32_t nbits, uint32_t value) { - uint32_t res = 0; - for (uint32_t i = 0; i < nbits; ++i) { - res = (res << 1) + (value & 1); - value >>= 1; - } - return res; -} - -/** - * @brief this computes the sequence: 0,1/2,1/4,3/4,1/8,5/8,3/8,7/8,... - * essentially: the bits of (i+1) in lsb order on the basis (1/2^k) mod 1*/ -double fracrevbits(uint32_t i) { - if (i == 0) return 0; - if (i == 1) return 0.5; - if (i % 2 == 0) - return fracrevbits(i / 2) / 2.; - else - return fracrevbits((i - 1) / 2) / 2. + 0.5; -} - -uint64_t ceilto64b(uint64_t size) { return (size + UINT64_C(63)) & (UINT64_C(-64)); } - -uint64_t ceilto32b(uint64_t size) { return (size + UINT64_C(31)) & (UINT64_C(-32)); }
@@ -1,72 +0,0 @@
-#ifndef SPQLIOS_COMMONS_PRIVATE_H -#define SPQLIOS_COMMONS_PRIVATE_H - -#include "commons.h" - -#ifdef __cplusplus -#include <cmath> -#include <cstdio> -#include <cstdlib> -#else -#include <math.h> -#include <stdio.h> -#include <stdlib.h> -#define nullptr 0x0; -#endif - -/** @brief log2 of a power of two (UB if m is not a power of two) */ -EXPORT uint32_t log2m(uint32_t m); - -/** @brief checks if the doublevalue is a power of two */ -EXPORT uint64_t is_not_pow2_double(void* doublevalue); - -#define UNDEFINED() \ - { \ - fprintf(stderr, "UNDEFINED!!!\n"); \ - abort(); \ - } -#define NOT_IMPLEMENTED() \ - { \ - fprintf(stderr, "NOT IMPLEMENTED!!!\n"); \ - abort(); \ - } -#define NOT_SUPPORTED() \ - { \ - fprintf(stderr, "NOT SUPPORTED!!!\n"); \ - abort(); \ - } -#define FATAL_ERROR(MESSAGE) \ - { \ - fprintf(stderr, "ERROR: %s\n", (MESSAGE)); \ - abort(); \ - } - -#define STATIC_ASSERT(condition) (void)sizeof(char[-1 + 2 * !!(condition)]) - -/** @brief reports the error and returns nullptr */ -EXPORT void* spqlios_error(const char* error); -/** @brief if ptr2 is not null, returns ptr, otherwise free ptr and return null */ -EXPORT void* spqlios_keep_or_free(void* ptr, void* ptr2); - -#ifdef __x86_64__ -#define CPU_SUPPORTS __builtin_cpu_supports -#else -// TODO for now, we do not have any optimization for non x86 targets -#define CPU_SUPPORTS(xxxx) 0 -#endif - -/** @brief returns the n bits of value in reversed order */ -EXPORT uint32_t revbits(uint32_t nbits, uint32_t value); - -/** - * @brief this computes the sequence: 0,1/2,1/4,3/4,1/8,5/8,3/8,7/8,... - * essentially: the bits of (i+1) in lsb order on the basis (1/2^k) mod 1*/ -EXPORT double fracrevbits(uint32_t i); - -/** @brief smallest multiple of 64 higher or equal to size */ -EXPORT uint64_t ceilto64b(uint64_t size); - -/** @brief smallest multiple of 32 higher or equal to size */ -EXPORT uint64_t ceilto32b(uint64_t size); - -#endif // SPQLIOS_COMMONS_PRIVATE_H
@@ -1,22 +0,0 @@
-In this folder, we deal with the full complex FFT in `C[X] mod X^M-i`. -One complex is represented by two consecutive doubles `(real,imag)` -Note that a real polynomial sum_{j=0}^{N-1} p_j.X^j mod X^N+1 -corresponds to the complex polynomial of half degree `M=N/2`: -`sum_{j=0}^{M-1} (p_{j} + i.p_{j+M}) X^j mod X^M-i` - -For a complex polynomial A(X) sum c_i X^i of degree M-1 -or a real polynomial sum a_i X^i of degree N - -coefficient space: -a_0,a_M,a_1,a_{M+1},...,a_{M-1},a_{2M-1} -or equivalently -Re(c_0),Im(c_0),Re(c_1),Im(c_1),...Re(c_{M-1}),Im(c_{M-1}) - -eval space: -c(omega_{0}),...,c(omega_{M-1}) - -where -omega_j = omega^{1+rev_{2N}(j)} -and omega = exp(i.pi/N) - -rev_{2N}(j) is the number that has the log2(2N) bits of j in reverse order.
@@ -1,80 +0,0 @@
-#include "cplx_fft_internal.h" - -void cplx_set(CPLX r, const CPLX a) { - r[0] = a[0]; - r[1] = a[1]; -} -void cplx_neg(CPLX r, const CPLX a) { - r[0] = -a[0]; - r[1] = -a[1]; -} -void cplx_add(CPLX r, const CPLX a, const CPLX b) { - r[0] = a[0] + b[0]; - r[1] = a[1] + b[1]; -} -void cplx_sub(CPLX r, const CPLX a, const CPLX b) { - r[0] = a[0] - b[0]; - r[1] = a[1] - b[1]; -} -void cplx_mul(CPLX r, const CPLX a, const CPLX b) { - double re = a[0] * b[0] - a[1] * b[1]; - r[1] = a[0] * b[1] + a[1] * b[0]; - r[0] = re; -} - -/** - * @brief splits 2h evaluations of one polynomials into 2 times h evaluations of even/odd polynomial - * Input: Q_0(y),...,Q_{h-1}(y),Q_0(-y),...,Q_{h-1}(-y) - * Output: P_0(z),...,P_{h-1}(z),P_h(z),...,P_{2h-1}(z) - * where Q_i(X)=P_i(X^2)+X.P_{h+i}(X^2) and y^2 = z - * @param h number of "coefficients" h >= 1 - * @param data 2h complex coefficients interleaved and 256b aligned - * @param powom y represented as (yre,yim) - */ -EXPORT void cplx_split_fft_ref(int32_t h, CPLX* data, const CPLX powom) { - CPLX* d0 = data; - CPLX* d1 = data + h; - for (uint64_t i = 0; i < h; ++i) { - CPLX diff; - cplx_sub(diff, d0[i], d1[i]); - cplx_add(d0[i], d0[i], d1[i]); - cplx_mul(d1[i], diff, powom); - } -} - -/** - * @brief Do two layers of itwiddle (i.e. split). - * Input/output: d0,d1,d2,d3 of length h - * Algo: - * itwiddle(d0,d1,om[0]),itwiddle(d2,d3,i.om[0]) - * itwiddle(d0,d2,om[1]),itwiddle(d1,d3,om[1]) - * @param h number of "coefficients" h >= 1 - * @param data 4h complex coefficients interleaved and 256b aligned - * @param powom om[0] (re,im) and om[1] where om[1]=om[0]^2 - */ -EXPORT void cplx_bisplit_fft_ref(int32_t h, CPLX* data, const CPLX powom[2]) { - CPLX* d0 = data; - CPLX* d2 = data + 2 * h; - const CPLX* om0 = powom; - CPLX iom0; - iom0[0] = powom[0][1]; - iom0[1] = -powom[0][0]; - const CPLX* om1 = powom + 1; - cplx_split_fft_ref(h, d0, *om0); - cplx_split_fft_ref(h, d2, iom0); - cplx_split_fft_ref(2 * h, d0, *om1); -} - -/** - * Input: Q(y),Q(-y) - * Output: P_0(z),P_1(z) - * where Q(X)=P_0(X^2)+X.P_1(X^2) and y^2 = z - * @param data 2 complexes coefficients interleaved and 256b aligned - * @param powom (z,-z) interleaved: (zre,zim,-zre,-zim) - */ -void split_fft_last_ref(CPLX* data, const CPLX powom) { - CPLX diff; - cplx_sub(diff, data[0], data[1]); - cplx_add(data[0], data[0], data[1]); - cplx_mul(data[1], diff, powom); -}
@@ -1,158 +0,0 @@
-#include <errno.h> -#include <string.h> - -#include "../commons_private.h" -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -EXPORT void cplx_from_znx32_ref(const CPLX_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x) { - const uint32_t m = precomp->m; - const int32_t* inre = x; - const int32_t* inim = x + m; - CPLX* out = r; - for (uint32_t i = 0; i < m; ++i) { - out[i][0] = (double)inre[i]; - out[i][1] = (double)inim[i]; - } -} - -EXPORT void cplx_from_tnx32_ref(const CPLX_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x) { - static const double _2p32 = 1. / (INT64_C(1) << 32); - const uint32_t m = precomp->m; - const int32_t* inre = x; - const int32_t* inim = x + m; - CPLX* out = r; - for (uint32_t i = 0; i < m; ++i) { - out[i][0] = ((double)inre[i]) * _2p32; - out[i][1] = ((double)inim[i]) * _2p32; - } -} - -EXPORT void cplx_to_tnx32_ref(const CPLX_TO_TNX32_PRECOMP* precomp, int32_t* r, const void* x) { - static const double _2p32 = (INT64_C(1) << 32); - const uint32_t m = precomp->m; - double factor = _2p32 / precomp->divisor; - int32_t* outre = r; - int32_t* outim = r + m; - const CPLX* in = x; - // Note: this formula will only work if abs(in) < 2^32 - for (uint32_t i = 0; i < m; ++i) { - outre[i] = (int32_t)(int64_t)(rint(in[i][0] * factor)); - outim[i] = (int32_t)(int64_t)(rint(in[i][1] * factor)); - } -} - -void* init_cplx_from_znx32_precomp(CPLX_FROM_ZNX32_PRECOMP* res, uint32_t m) { - res->m = m; - if (CPU_SUPPORTS("avx2")) { - if (m >= 8) { - res->function = cplx_from_znx32_avx2_fma; - } else { - res->function = cplx_from_znx32_ref; - } - } else { - res->function = cplx_from_znx32_ref; - } - return res; -} - -CPLX_FROM_ZNX32_PRECOMP* new_cplx_from_znx32_precomp(uint32_t m) { - CPLX_FROM_ZNX32_PRECOMP* res = malloc(sizeof(CPLX_FROM_ZNX32_PRECOMP)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_cplx_from_znx32_precomp(res, m)); -} - -void* init_cplx_from_tnx32_precomp(CPLX_FROM_TNX32_PRECOMP* res, uint32_t m) { - res->m = m; - if (CPU_SUPPORTS("avx2")) { - if (m >= 8) { - res->function = cplx_from_tnx32_avx2_fma; - } else { - res->function = cplx_from_tnx32_ref; - } - } else { - res->function = cplx_from_tnx32_ref; - } - return res; -} - -CPLX_FROM_TNX32_PRECOMP* new_cplx_from_tnx32_precomp(uint32_t m) { - CPLX_FROM_TNX32_PRECOMP* res = malloc(sizeof(CPLX_FROM_TNX32_PRECOMP)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_cplx_from_tnx32_precomp(res, m)); -} - -void* init_cplx_to_tnx32_precomp(CPLX_TO_TNX32_PRECOMP* res, uint32_t m, double divisor, uint32_t log2overhead) { - if (is_not_pow2_double(&divisor)) return spqlios_error("divisor must be a power of 2"); - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - if (log2overhead > 52) return spqlios_error("log2overhead is too large"); - res->m = m; - res->divisor = divisor; - if (CPU_SUPPORTS("avx2")) { - if (log2overhead <= 18) { - if (m >= 8) { - res->function = cplx_to_tnx32_avx2_fma; - } else { - res->function = cplx_to_tnx32_ref; - } - } else { - res->function = cplx_to_tnx32_ref; - } - } else { - res->function = cplx_to_tnx32_ref; - } - return res; -} - -EXPORT CPLX_TO_TNX32_PRECOMP* new_cplx_to_tnx32_precomp(uint32_t m, double divisor, uint32_t log2overhead) { - CPLX_TO_TNX32_PRECOMP* res = malloc(sizeof(CPLX_TO_TNX32_PRECOMP)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_cplx_to_tnx32_precomp(res, m, divisor, log2overhead)); -} - -/** - * @brief Simpler API for the znx32 to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_from_znx32_simple(uint32_t m, void* r, const int32_t* x) { - // not checking for log2bound which is not relevant here - static CPLX_FROM_ZNX32_PRECOMP precomp[32]; - CPLX_FROM_ZNX32_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_cplx_from_znx32_precomp(p, m)) abort(); - } - p->function(p, r, x); -} - -/** - * @brief Simpler API for the tnx32 to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_from_tnx32_simple(uint32_t m, void* r, const int32_t* x) { - static CPLX_FROM_TNX32_PRECOMP precomp[32]; - CPLX_FROM_TNX32_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_cplx_from_tnx32_precomp(p, m)) abort(); - } - p->function(p, r, x); -} -/** - * @brief Simpler API for the cplx to tnx32 conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_to_tnx32_simple(uint32_t m, double divisor, uint32_t log2overhead, int32_t* r, const void* x) { - struct LAST_CPLX_TO_TNX32_PRECOMP { - CPLX_TO_TNX32_PRECOMP p; - double last_divisor; - double last_log2over; - }; - static __thread struct LAST_CPLX_TO_TNX32_PRECOMP precomp[32]; - struct LAST_CPLX_TO_TNX32_PRECOMP* p = precomp + log2m(m); - if (!p->p.function || divisor != p->last_divisor || log2overhead != p->last_log2over) { - memset(p, 0, sizeof(*p)); - if (!init_cplx_to_tnx32_precomp(&p->p, m, divisor, log2overhead)) abort(); - p->last_divisor = divisor; - p->last_log2over = log2overhead; - } - p->p.function(&p->p, r, x); -}
@@ -1,104 +0,0 @@
-#include <immintrin.h> -#include <stdio.h> - -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -typedef int32_t I8MEM[8]; -typedef double D4MEM[4]; - -__always_inline void cplx_from_any_fma(uint64_t m, void* r, const int32_t* x, const __m256i C, const __m256d R) { - const __m256i S = _mm256_set1_epi32(0x80000000); - const I8MEM* inre = (I8MEM*)(x); - const I8MEM* inim = (I8MEM*)(x + m); - D4MEM* out = (D4MEM*)r; - const uint64_t ms8 = m / 8; - for (uint32_t i = 0; i < ms8; ++i) { - __m256i rea = _mm256_loadu_si256((__m256i*)inre[0]); - __m256i ima = _mm256_loadu_si256((__m256i*)inim[0]); - rea = _mm256_add_epi32(rea, S); - ima = _mm256_add_epi32(ima, S); - __m256i tmpa = _mm256_unpacklo_epi32(rea, ima); - __m256i tmpc = _mm256_unpackhi_epi32(rea, ima); - __m256i cpla = _mm256_permute2x128_si256(tmpa, tmpc, 0x20); - __m256i cplc = _mm256_permute2x128_si256(tmpa, tmpc, 0x31); - tmpa = _mm256_unpacklo_epi32(cpla, C); - __m256i tmpb = _mm256_unpackhi_epi32(cpla, C); - tmpc = _mm256_unpacklo_epi32(cplc, C); - __m256i tmpd = _mm256_unpackhi_epi32(cplc, C); - cpla = _mm256_permute2x128_si256(tmpa, tmpb, 0x20); - __m256i cplb = _mm256_permute2x128_si256(tmpa, tmpb, 0x31); - cplc = _mm256_permute2x128_si256(tmpc, tmpd, 0x20); - __m256i cpld = _mm256_permute2x128_si256(tmpc, tmpd, 0x31); - __m256d dcpla = _mm256_sub_pd(_mm256_castsi256_pd(cpla), R); - __m256d dcplb = _mm256_sub_pd(_mm256_castsi256_pd(cplb), R); - __m256d dcplc = _mm256_sub_pd(_mm256_castsi256_pd(cplc), R); - __m256d dcpld = _mm256_sub_pd(_mm256_castsi256_pd(cpld), R); - _mm256_storeu_pd(out[0], dcpla); - _mm256_storeu_pd(out[1], dcplb); - _mm256_storeu_pd(out[2], dcplc); - _mm256_storeu_pd(out[3], dcpld); - inre += 1; - inim += 1; - out += 4; - } -} - -EXPORT void cplx_from_znx32_avx2_fma(const CPLX_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x) { - // note: the hex code of 2^31 + 2^52 is 0x4330000080000000 - const __m256i C = _mm256_set1_epi32(0x43300000); - const __m256d R = _mm256_set1_pd((INT64_C(1) << 31) + (INT64_C(1) << 52)); - // double XX = INT64_C(1) + (INT64_C(1)<<31) + (INT64_C(1)<<52); - // printf("\n\n%016lx\n", *(uint64_t*)&XX); - // abort(); - const uint64_t m = precomp->m; - cplx_from_any_fma(m, r, x, C, R); -} - -EXPORT void cplx_from_tnx32_avx2_fma(const CPLX_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x) { - // note: the hex code of 2^-1 + 2^30 is 0x4130000080000000 - const __m256i C = _mm256_set1_epi32(0x41300000); - const __m256d R = _mm256_set1_pd(0.5 + (INT64_C(1) << 20)); - // double XX = (double)(INT64_C(1) + (INT64_C(1)<<31) + (INT64_C(1)<<52))/(INT64_C(1)<<32); - // printf("\n\n%016lx\n", *(uint64_t*)&XX); - // abort(); - const uint64_t m = precomp->m; - cplx_from_any_fma(m, r, x, C, R); -} - -EXPORT void cplx_to_tnx32_avx2_fma(const CPLX_TO_TNX32_PRECOMP* precomp, int32_t* r, const void* x) { - const __m256d R = _mm256_set1_pd((0.5 + (INT64_C(3) << 19)) * precomp->divisor); - const __m256i MASK = _mm256_set1_epi64x(0xFFFFFFFFUL); - const __m256i S = _mm256_set1_epi32(0x80000000); - // const __m256i IDX = _mm256_set_epi32(0,4,1,5,2,6,3,7); - const __m256i IDX = _mm256_set_epi32(7, 3, 6, 2, 5, 1, 4, 0); - const uint64_t m = precomp->m; - const uint64_t ms8 = m / 8; - I8MEM* outre = (I8MEM*)r; - I8MEM* outim = (I8MEM*)(r + m); - const D4MEM* in = x; - // Note: this formula will only work if abs(in) < 2^32 - for (uint32_t i = 0; i < ms8; ++i) { - __m256d cpla = _mm256_loadu_pd(in[0]); - __m256d cplb = _mm256_loadu_pd(in[1]); - __m256d cplc = _mm256_loadu_pd(in[2]); - __m256d cpld = _mm256_loadu_pd(in[3]); - __m256i icpla = _mm256_castpd_si256(_mm256_add_pd(cpla, R)); - __m256i icplb = _mm256_castpd_si256(_mm256_add_pd(cplb, R)); - __m256i icplc = _mm256_castpd_si256(_mm256_add_pd(cplc, R)); - __m256i icpld = _mm256_castpd_si256(_mm256_add_pd(cpld, R)); - icpla = _mm256_or_si256(_mm256_and_si256(icpla, MASK), _mm256_slli_epi64(icplb, 32)); - icplc = _mm256_or_si256(_mm256_and_si256(icplc, MASK), _mm256_slli_epi64(icpld, 32)); - icpla = _mm256_xor_si256(icpla, S); - icplc = _mm256_xor_si256(icplc, S); - __m256i re = _mm256_unpacklo_epi64(icpla, icplc); - __m256i im = _mm256_unpackhi_epi64(icpla, icplc); - re = _mm256_permutevar8x32_epi32(re, IDX); - im = _mm256_permutevar8x32_epi32(im, IDX); - _mm256_storeu_si256((__m256i*)outre[0], re); - _mm256_storeu_si256((__m256i*)outim[0], im); - outre += 1; - outim += 1; - in += 4; - } -}
@@ -1,18 +0,0 @@
-#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -EXPORT void cplx_from_znx32(const CPLX_FROM_ZNX32_PRECOMP* tables, void* r, const int32_t* a) { - tables->function(tables, r, a); -} -EXPORT void cplx_from_tnx32(const CPLX_FROM_TNX32_PRECOMP* tables, void* r, const int32_t* a) { - tables->function(tables, r, a); -} -EXPORT void cplx_to_tnx32(const CPLX_TO_TNX32_PRECOMP* tables, int32_t* r, const void* a) { - tables->function(tables, r, a); -} -EXPORT void cplx_fftvec_mul(const CPLX_FFTVEC_MUL_PRECOMP* tables, void* r, const void* a, const void* b) { - tables->function(tables, r, a, b); -} -EXPORT void cplx_fftvec_addmul(const CPLX_FFTVEC_ADDMUL_PRECOMP* tables, void* r, const void* a, const void* b) { - tables->function(tables, r, a, b); -}
@@ -1,43 +0,0 @@
-#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -EXPORT void cplx_fftvec_addmul_fma(const CPLX_FFTVEC_ADDMUL_PRECOMP* tables, void* r, const void* a, const void* b) { - UNDEFINED(); // not defined for non x86 targets -} -EXPORT void cplx_fftvec_mul_fma(const CPLX_FFTVEC_MUL_PRECOMP* tables, void* r, const void* a, const void* b) { - UNDEFINED(); -} -EXPORT void cplx_fftvec_addmul_sse(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - UNDEFINED(); -} -EXPORT void cplx_fftvec_addmul_avx512(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, - const void* b) { - UNDEFINED(); -} -EXPORT void cplx_fft16_avx_fma(void* data, const void* omega) { UNDEFINED(); } -EXPORT void cplx_ifft16_avx_fma(void* data, const void* omega) { UNDEFINED(); } -EXPORT void cplx_from_znx32_avx2_fma(const CPLX_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x) { UNDEFINED(); } -EXPORT void cplx_from_tnx32_avx2_fma(const CPLX_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x) { UNDEFINED(); } -EXPORT void cplx_to_tnx32_avx2_fma(const CPLX_TO_TNX32_PRECOMP* precomp, int32_t* x, const void* c) { UNDEFINED(); } -EXPORT void cplx_fft_avx2_fma(const CPLX_FFT_PRECOMP* tables, void* data){UNDEFINED()} EXPORT - void cplx_ifft_avx2_fma(const CPLX_IFFT_PRECOMP* itables, void* data){UNDEFINED()} EXPORT - void cplx_fftvec_twiddle_fma(const CPLX_FFTVEC_TWIDDLE_PRECOMP* tables, void* a, void* b, const void* om){ - UNDEFINED()} EXPORT void cplx_fftvec_twiddle_avx512(const CPLX_FFTVEC_TWIDDLE_PRECOMP* tables, void* a, void* b, - const void* om){UNDEFINED()} EXPORT - void cplx_fftvec_bitwiddle_fma(const CPLX_FFTVEC_BITWIDDLE_PRECOMP* tables, void* a, uint64_t slice, - const void* om){UNDEFINED()} EXPORT - void cplx_fftvec_bitwiddle_avx512(const CPLX_FFTVEC_BITWIDDLE_PRECOMP* tables, void* a, uint64_t slice, - const void* om){UNDEFINED()} - -// DEPRECATED? -EXPORT void cplx_fftvec_add_fma(uint32_t m, void* r, const void* a, const void* b){UNDEFINED()} EXPORT - void cplx_fftvec_sub2_to_fma(uint32_t m, void* r, const void* a, const void* b){UNDEFINED()} EXPORT - void cplx_fftvec_copy_fma(uint32_t m, void* r, const void* a) { - UNDEFINED() -} - -// executors -// EXPORT void cplx_ifft(const CPLX_IFFT_PRECOMP* itables, void* data) { -// itables->function(itables, data); -//} -// EXPORT void cplx_fft(const CPLX_FFT_PRECOMP* tables, void* data) { tables->function(tables, data); }
@@ -1,221 +0,0 @@
-#ifndef SPQLIOS_CPLX_FFT_H -#define SPQLIOS_CPLX_FFT_H - -#include "../commons.h" - -typedef struct cplx_fft_precomp CPLX_FFT_PRECOMP; -typedef struct cplx_ifft_precomp CPLX_IFFT_PRECOMP; -typedef struct cplx_mul_precomp CPLX_FFTVEC_MUL_PRECOMP; -typedef struct cplx_addmul_precomp CPLX_FFTVEC_ADDMUL_PRECOMP; -typedef struct cplx_from_znx32_precomp CPLX_FROM_ZNX32_PRECOMP; -typedef struct cplx_from_tnx32_precomp CPLX_FROM_TNX32_PRECOMP; -typedef struct cplx_to_tnx32_precomp CPLX_TO_TNX32_PRECOMP; -typedef struct cplx_to_znx32_precomp CPLX_TO_ZNX32_PRECOMP; -typedef struct cplx_from_rnx64_precomp CPLX_FROM_RNX64_PRECOMP; -typedef struct cplx_to_rnx64_precomp CPLX_TO_RNX64_PRECOMP; -typedef struct cplx_round_to_rnx64_precomp CPLX_ROUND_TO_RNX64_PRECOMP; - -/** - * @brief precomputes fft tables. - * The FFT tables contains a constant section that is required for efficient FFT operations in dimension nn. - * The resulting pointer is to be passed as "tables" argument to any call to the fft function. - * The user can optionnally allocate zero or more computation buffers, which are scratch spaces that are contiguous to - * the constant tables in memory, and allow for more efficient operations. It is the user's responsibility to ensure - * that each of those buffers are never used simultaneously by two ffts on different threads at the same time. The fft - * table must be deleted by delete_fft_precomp after its last usage. - */ -EXPORT CPLX_FFT_PRECOMP* new_cplx_fft_precomp(uint32_t m, uint32_t num_buffers); - -/** - * @brief gets the address of a fft buffer allocated during new_fft_precomp. - * This buffer can be used as data pointer in subsequent calls to fft, - * and does not need to be released afterwards. - */ -EXPORT void* cplx_fft_precomp_get_buffer(const CPLX_FFT_PRECOMP* tables, uint32_t buffer_index); - -/** - * @brief allocates a new fft buffer. - * This buffer can be used as data pointer in subsequent calls to fft, - * and must be deleted afterwards by calling delete_fft_buffer. - */ -EXPORT void* new_cplx_fft_buffer(uint32_t m); - -/** - * @brief allocates a new fft buffer. - * This buffer can be used as data pointer in subsequent calls to fft, - * and must be deleted afterwards by calling delete_fft_buffer. - */ -EXPORT void delete_cplx_fft_buffer(void* buffer); - -/** - * @brief deallocates a fft table and all its built-in buffers. - */ -#define delete_cplx_fft_precomp free - -/** - * @brief computes a direct fft in-place over data. - */ -EXPORT void cplx_fft(const CPLX_FFT_PRECOMP* tables, void* data); - -EXPORT CPLX_IFFT_PRECOMP* new_cplx_ifft_precomp(uint32_t m, uint32_t num_buffers); -EXPORT void* cplx_ifft_precomp_get_buffer(const CPLX_IFFT_PRECOMP* tables, uint32_t buffer_index); -EXPORT void cplx_ifft(const CPLX_IFFT_PRECOMP* tables, void* data); -#define delete_cplx_ifft_precomp free - -EXPORT CPLX_FFTVEC_MUL_PRECOMP* new_cplx_fftvec_mul_precomp(uint32_t m); -EXPORT void cplx_fftvec_mul(const CPLX_FFTVEC_MUL_PRECOMP* tables, void* r, const void* a, const void* b); -#define delete_cplx_fftvec_mul_precomp free - -EXPORT CPLX_FFTVEC_ADDMUL_PRECOMP* new_cplx_fftvec_addmul_precomp(uint32_t m); -EXPORT void cplx_fftvec_addmul(const CPLX_FFTVEC_ADDMUL_PRECOMP* tables, void* r, const void* a, const void* b); -#define delete_cplx_fftvec_addmul_precomp free - -/** - * @brief prepares a conversion from ZnX to the cplx layout. - * All the coefficients must be strictly lower than 2^log2bound in absolute value. Any attempt to use - * this function on a larger coefficient is undefined behaviour. The resulting precomputed data must - * be freed with `new_cplx_from_znx32_precomp` - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the inputs have n=2m - * int32 coefficients in natural order modulo X^n+1 - * @param log2bound bound on the input coefficients. Must be between 0 and 32 - */ -EXPORT CPLX_FROM_ZNX32_PRECOMP* new_cplx_from_znx32_precomp(uint32_t m); -/** - * @brief converts from ZnX to the cplx layout. - * @param tables precomputed data obtained by new_cplx_from_znx32_precomp. - * @param r resulting array of m complexes coefficients mod X^m-i - * @param x input array of n bounded integer coefficients mod X^n+1 - */ -EXPORT void cplx_from_znx32(const CPLX_FROM_ZNX32_PRECOMP* tables, void* r, const int32_t* a); -/** @brief frees a precomputed conversion data initialized with new_cplx_from_znx32_precomp. */ -#define delete_cplx_from_znx32_precomp free - -/** - * @brief prepares a conversion from TnX to the cplx layout. - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the inputs have n=2m - * torus32 coefficients. The resulting precomputed data must - * be freed with `delete_cplx_from_tnx32_precomp` - */ -EXPORT CPLX_FROM_TNX32_PRECOMP* new_cplx_from_tnx32_precomp(uint32_t m); -/** - * @brief converts from TnX to the cplx layout. - * @param tables precomputed data obtained by new_cplx_from_tnx32_precomp. - * @param r resulting array of m complexes coefficients mod X^m-i - * @param x input array of n torus32 coefficients mod X^n+1 - */ -EXPORT void cplx_from_tnx32(const CPLX_FROM_TNX32_PRECOMP* tables, void* r, const int32_t* a); -/** @brief frees a precomputed conversion data initialized with new_cplx_from_tnx32_precomp. */ -#define delete_cplx_from_tnx32_precomp free - -/** - * @brief prepares a rescale and conversion from the cplx layout to TnX. - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the outputs have n=2m - * torus32 coefficients. - * @param divisor must be a power of two. The inputs are rescaled by divisor before being reduced modulo 1. - * Remember that the output of an iFFT must be divided by m. - * @param log2overhead all inputs absolute values must be within divisor.2^log2overhead. - * For any inputs outside of these bounds, the conversion is undefined behaviour. - * The maximum supported log2overhead is 52, and the algorithm is faster for log2overhead=18. - */ -EXPORT CPLX_TO_TNX32_PRECOMP* new_cplx_to_tnx32_precomp(uint32_t m, double divisor, uint32_t log2overhead); -/** - * @brief rescale, converts and reduce mod 1 from cplx layout to torus32. - * @param tables precomputed data obtained by new_cplx_from_tnx32_precomp. - * @param r resulting array of n torus32 coefficients mod X^n+1 - * @param x input array of m cplx coefficients mod X^m-i - */ -EXPORT void cplx_to_tnx32(const CPLX_TO_TNX32_PRECOMP* tables, int32_t* r, const void* a); -#define delete_cplx_to_tnx32_precomp free - -EXPORT CPLX_TO_ZNX32_PRECOMP* new_cplx_to_znx32_precomp(uint32_t m, double divisor); -EXPORT void cplx_to_znx32(const CPLX_TO_ZNX32_PRECOMP* precomp, int32_t* r, const void* x); -#define delete_cplx_to_znx32_simple free - -EXPORT CPLX_FROM_RNX64_PRECOMP* new_cplx_from_rnx64_simple(uint32_t m); -EXPORT void cplx_from_rnx64(const CPLX_FROM_RNX64_PRECOMP* precomp, void* r, const double* x); -#define delete_cplx_from_rnx64_simple free - -EXPORT CPLX_TO_RNX64_PRECOMP* new_cplx_to_rnx64(uint32_t m, double divisor); -EXPORT void cplx_to_rnx64(const CPLX_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -#define delete_cplx_round_to_rnx64_simple free - -EXPORT CPLX_ROUND_TO_RNX64_PRECOMP* new_cplx_round_to_rnx64(uint32_t m, double divisor, uint32_t log2bound); -EXPORT void cplx_round_to_rnx64(const CPLX_ROUND_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -#define delete_cplx_round_to_rnx64_simple free - -/** - * @brief Simpler API for the fft function. - * For each dimension, the precomputed tables for this dimension are generated automatically. - * It is advised to do one dry-run per desired dimension before using in a multithread environment */ -EXPORT void cplx_fft_simple(uint32_t m, void* data); -/** - * @brief Simpler API for the ifft function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension in the main thread before using in a multithread - * environment */ -EXPORT void cplx_ifft_simple(uint32_t m, void* data); -/** - * @brief Simpler API for the fftvec multiplication function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_fftvec_mul_simple(uint32_t m, void* r, const void* a, const void* b); -/** - * @brief Simpler API for the fftvec addmul function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_fftvec_addmul_simple(uint32_t m, void* r, const void* a, const void* b); -/** - * @brief Simpler API for the znx32 to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_from_znx32_simple(uint32_t m, void* r, const int32_t* x); -/** - * @brief Simpler API for the tnx32 to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_from_tnx32_simple(uint32_t m, void* r, const int32_t* x); -/** - * @brief Simpler API for the cplx to tnx32 conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void cplx_to_tnx32_simple(uint32_t m, double divisor, uint32_t log2overhead, int32_t* r, const void* x); - -/** - * @brief converts, divides and round from cplx to znx32 (simple API) - * @param m the complex dimension - * @param divisor the divisor: a power of two, often m after an ifft - * @param r the result: must be a double array of size 2m. r must be distinct from x - * @param x the input: must hold m complex numbers. - */ -EXPORT void cplx_to_znx32_simple(uint32_t m, double divisor, int32_t* r, const void* x); - -/** - * @brief converts from rnx64 to cplx (simple API) - * The bound on the output is assumed to be within ]2^-31,2^31[. - * Any coefficient that would fall outside this range is undefined behaviour. - * @param m the complex dimension - * @param r the result: must be an array of m complex numbers. r must be distinct from x - * @param x the input: must be an array of 2m doubles. - */ -EXPORT void cplx_from_rnx64_simple(uint32_t m, void* r, const double* x); - -/** - * @brief converts, divides from cplx to rnx64 (simple API) - * @param m the complex dimension - * @param divisor the divisor: a power of two, often m after an ifft - * @param r the result: must be a double array of size 2m. r must be distinct from x - * @param x the input: must hold m complex numbers. - */ -EXPORT void cplx_to_rnx64_simple(uint32_t m, double divisor, double* r, const void* x); - -/** - * @brief converts, divides and round to integer from cplx to rnx32 (simple API) - * @param m the complex dimension - * @param divisor the divisor: a power of two, often m after an ifft - * @param log2bound a guarantee on the log2bound of the output. log2bound<=48 will use a more efficient algorithm. - * @param r the result: must be a double array of size 2m. r must be distinct from x - * @param x the input: must hold m complex numbers. - */ -EXPORT void cplx_round_to_rnx64_simple(uint32_t m, double divisor, uint32_t log2bound, double* r, const void* x); - -#endif // SPQLIOS_CPLX_FFT_H
@@ -1,156 +0,0 @@
-# shifted FFT over X^16-i -# 1st argument (rdi) contains 16 complexes -# 2nd argument (rsi) contains: 8 complexes -# omega,alpha,beta,j.beta,gamma,j.gamma,k.gamma,kj.gamma -# alpha = sqrt(omega), beta = sqrt(alpha), gamma = sqrt(beta) -# j = sqrt(i), k=sqrt(j) -.globl cplx_fft16_avx_fma -cplx_fft16_avx_fma: -vmovupd (%rdi),%ymm8 -vmovupd 0x20(%rdi),%ymm9 -vmovupd 0x40(%rdi),%ymm10 -vmovupd 0x60(%rdi),%ymm11 -vmovupd 0x80(%rdi),%ymm12 -vmovupd 0xa0(%rdi),%ymm13 -vmovupd 0xc0(%rdi),%ymm14 -vmovupd 0xe0(%rdi),%ymm15 - -.first_pass: -vmovupd (%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vshufpd $5, %ymm12, %ymm12, %ymm4 -vshufpd $5, %ymm13, %ymm13, %ymm5 -vshufpd $5, %ymm14, %ymm14, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm1,%ymm5 -vmulpd %ymm6,%ymm1,%ymm6 -vmulpd %ymm7,%ymm1,%ymm7 -vfmaddsub231pd %ymm12, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm12) +/- ymm4 -vfmaddsub231pd %ymm13, %ymm0, %ymm5 -vfmaddsub231pd %ymm14, %ymm0, %ymm6 -vfmaddsub231pd %ymm15, %ymm0, %ymm7 -vsubpd %ymm4,%ymm8,%ymm12 -vsubpd %ymm5,%ymm9,%ymm13 -vsubpd %ymm6,%ymm10,%ymm14 -vsubpd %ymm7,%ymm11,%ymm15 -vaddpd %ymm4,%ymm8,%ymm8 -vaddpd %ymm5,%ymm9,%ymm9 -vaddpd %ymm6,%ymm10,%ymm10 -vaddpd %ymm7,%ymm11,%ymm11 - -.second_pass: -vmovupd 16(%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vshufpd $5, %ymm10, %ymm10, %ymm4 -vshufpd $5, %ymm11, %ymm11, %ymm5 -vshufpd $5, %ymm14, %ymm14, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm1,%ymm5 -vmulpd %ymm6,%ymm0,%ymm6 -vmulpd %ymm7,%ymm0,%ymm7 -vfmaddsub231pd %ymm10, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm10) +/- ymm4 -vfmaddsub231pd %ymm11, %ymm0, %ymm5 -vfmsubadd231pd %ymm14, %ymm1, %ymm6 -vfmsubadd231pd %ymm15, %ymm1, %ymm7 -vsubpd %ymm4,%ymm8,%ymm10 -vsubpd %ymm5,%ymm9,%ymm11 -vaddpd %ymm6,%ymm12,%ymm14 -vaddpd %ymm7,%ymm13,%ymm15 -vaddpd %ymm4,%ymm8,%ymm8 -vaddpd %ymm5,%ymm9,%ymm9 -vsubpd %ymm6,%ymm12,%ymm12 -vsubpd %ymm7,%ymm13,%ymm13 - -.third_pass: -vmovupd 32(%rsi),%xmm0 /* gamma */ -vmovupd 48(%rsi),%xmm2 /* delta */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 -vinsertf128 $1, %xmm2, %ymm2, %ymm2 -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vshufpd $5, %ymm9, %ymm9, %ymm4 -vshufpd $5, %ymm11, %ymm11, %ymm5 -vshufpd $5, %ymm13, %ymm13, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm0,%ymm5 -vmulpd %ymm6,%ymm3,%ymm6 -vmulpd %ymm7,%ymm2,%ymm7 -vfmaddsub231pd %ymm9, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm10) +/- ymm4 -vfmsubadd231pd %ymm11, %ymm1, %ymm5 -vfmaddsub231pd %ymm13, %ymm2, %ymm6 -vfmsubadd231pd %ymm15, %ymm3, %ymm7 -vsubpd %ymm4,%ymm8,%ymm9 -vaddpd %ymm5,%ymm10,%ymm11 -vsubpd %ymm6,%ymm12,%ymm13 -vaddpd %ymm7,%ymm14,%ymm15 -vaddpd %ymm4,%ymm8,%ymm8 -vsubpd %ymm5,%ymm10,%ymm10 -vaddpd %ymm6,%ymm12,%ymm12 -vsubpd %ymm7,%ymm14,%ymm14 - -.fourth_pass: -vmovupd 64(%rsi),%ymm0 /* gamma */ -vmovupd 96(%rsi),%ymm2 /* delta */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vperm2f128 $0x31,%ymm10,%ymm8,%ymm4 # ymm4 contains c1,c5 -- x gamma -vperm2f128 $0x31,%ymm11,%ymm9,%ymm5 # ymm5 contains c3,c7 -- x igamma -vperm2f128 $0x31,%ymm14,%ymm12,%ymm6 # ymm6 contains c9,c13 -- x delta -vperm2f128 $0x31,%ymm15,%ymm13,%ymm7 # ymm7 contains c11,c15 -- x idelta -vperm2f128 $0x20,%ymm10,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm11,%ymm9,%ymm9 # ymm9 contains c2,c6 -vperm2f128 $0x20,%ymm14,%ymm12,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x20,%ymm15,%ymm13,%ymm11 # ymm11 contains c10,c14 -vshufpd $5, %ymm4, %ymm4, %ymm12 -vshufpd $5, %ymm5, %ymm5, %ymm13 -vshufpd $5, %ymm6, %ymm6, %ymm14 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm12,%ymm1,%ymm12 -vmulpd %ymm13,%ymm0,%ymm13 -vmulpd %ymm14,%ymm3,%ymm14 -vmulpd %ymm15,%ymm2,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm12 # ymm12 = (ymm0 * ymm4) +/- ymm12 -vfmsubadd231pd %ymm5, %ymm1, %ymm13 -vfmaddsub231pd %ymm6, %ymm2, %ymm14 -vfmsubadd231pd %ymm7, %ymm3, %ymm15 -vsubpd %ymm12,%ymm8,%ymm4 -vaddpd %ymm13,%ymm9,%ymm5 -vsubpd %ymm14,%ymm10,%ymm6 -vaddpd %ymm15,%ymm11,%ymm7 -vaddpd %ymm12,%ymm8,%ymm8 -vsubpd %ymm13,%ymm9,%ymm9 -vaddpd %ymm14,%ymm10,%ymm10 -vsubpd %ymm15,%ymm11,%ymm11 - -vperm2f128 $0x20,%ymm6,%ymm10,%ymm12 # ymm4 contains c1,c5 -- x gamma -vperm2f128 $0x20,%ymm7,%ymm11,%ymm13 # ymm5 contains c3,c7 -- x igamma -vperm2f128 $0x31,%ymm6,%ymm10,%ymm14 # ymm6 contains c9,c13 -- x delta -vperm2f128 $0x31,%ymm7,%ymm11,%ymm15 # ymm7 contains c11,c15 -- x idelta -vperm2f128 $0x31,%ymm4,%ymm8,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x31,%ymm5,%ymm9,%ymm11 # ymm11 contains c10,c14 -vperm2f128 $0x20,%ymm4,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm5,%ymm9,%ymm9 # ymm9 contains c2,c6 - -.save_and_return: -vmovupd %ymm8,(%rdi) -vmovupd %ymm9,0x20(%rdi) -vmovupd %ymm10,0x40(%rdi) -vmovupd %ymm11,0x60(%rdi) -vmovupd %ymm12,0x80(%rdi) -vmovupd %ymm13,0xa0(%rdi) -vmovupd %ymm14,0xc0(%rdi) -vmovupd %ymm15,0xe0(%rdi) -ret -.size cplx_fft16_avx_fma, .-cplx_fft16_avx_fma -.section .note.GNU-stack,"",@progbits
@@ -1,190 +0,0 @@
- .text - .p2align 4 - .globl cplx_fft16_avx_fma - .def cplx_fft16_avx_fma; .scl 2; .type 32; .endef -cplx_fft16_avx_fma: - - pushq %rdi - pushq %rsi - movq %rcx,%rdi - movq %rdx,%rsi - subq $0x100,%rsp - movdqu %xmm6,(%rsp) - movdqu %xmm7,0x10(%rsp) - movdqu %xmm8,0x20(%rsp) - movdqu %xmm9,0x30(%rsp) - movdqu %xmm10,0x40(%rsp) - movdqu %xmm11,0x50(%rsp) - movdqu %xmm12,0x60(%rsp) - movdqu %xmm13,0x70(%rsp) - movdqu %xmm14,0x80(%rsp) - movdqu %xmm15,0x90(%rsp) - callq cplx_fft16_avx_fma_amd64 - movdqu (%rsp),%xmm6 - movdqu 0x10(%rsp),%xmm7 - movdqu 0x20(%rsp),%xmm8 - movdqu 0x30(%rsp),%xmm9 - movdqu 0x40(%rsp),%xmm10 - movdqu 0x50(%rsp),%xmm11 - movdqu 0x60(%rsp),%xmm12 - movdqu 0x70(%rsp),%xmm13 - movdqu 0x80(%rsp),%xmm14 - movdqu 0x90(%rsp),%xmm15 - addq $0x100,%rsp - popq %rsi - popq %rdi - retq - -# shifted FFT over X^16-i -# 1st argument (rdi) contains 16 complexes -# 2nd argument (rsi) contains: 8 complexes -# omega,alpha,beta,j.beta,gamma,j.gamma,k.gamma,kj.gamma -# alpha = sqrt(omega), beta = sqrt(alpha), gamma = sqrt(beta) -# j = sqrt(i), k=sqrt(j) -cplx_fft16_avx_fma_amd64: -vmovupd (%rdi),%ymm8 -vmovupd 0x20(%rdi),%ymm9 -vmovupd 0x40(%rdi),%ymm10 -vmovupd 0x60(%rdi),%ymm11 -vmovupd 0x80(%rdi),%ymm12 -vmovupd 0xa0(%rdi),%ymm13 -vmovupd 0xc0(%rdi),%ymm14 -vmovupd 0xe0(%rdi),%ymm15 - -.first_pass: -vmovupd (%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vshufpd $5, %ymm12, %ymm12, %ymm4 -vshufpd $5, %ymm13, %ymm13, %ymm5 -vshufpd $5, %ymm14, %ymm14, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm1,%ymm5 -vmulpd %ymm6,%ymm1,%ymm6 -vmulpd %ymm7,%ymm1,%ymm7 -vfmaddsub231pd %ymm12, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm12) +/- ymm4 -vfmaddsub231pd %ymm13, %ymm0, %ymm5 -vfmaddsub231pd %ymm14, %ymm0, %ymm6 -vfmaddsub231pd %ymm15, %ymm0, %ymm7 -vsubpd %ymm4,%ymm8,%ymm12 -vsubpd %ymm5,%ymm9,%ymm13 -vsubpd %ymm6,%ymm10,%ymm14 -vsubpd %ymm7,%ymm11,%ymm15 -vaddpd %ymm4,%ymm8,%ymm8 -vaddpd %ymm5,%ymm9,%ymm9 -vaddpd %ymm6,%ymm10,%ymm10 -vaddpd %ymm7,%ymm11,%ymm11 - -.second_pass: -vmovupd 16(%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vshufpd $5, %ymm10, %ymm10, %ymm4 -vshufpd $5, %ymm11, %ymm11, %ymm5 -vshufpd $5, %ymm14, %ymm14, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm1,%ymm5 -vmulpd %ymm6,%ymm0,%ymm6 -vmulpd %ymm7,%ymm0,%ymm7 -vfmaddsub231pd %ymm10, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm10) +/- ymm4 -vfmaddsub231pd %ymm11, %ymm0, %ymm5 -vfmsubadd231pd %ymm14, %ymm1, %ymm6 -vfmsubadd231pd %ymm15, %ymm1, %ymm7 -vsubpd %ymm4,%ymm8,%ymm10 -vsubpd %ymm5,%ymm9,%ymm11 -vaddpd %ymm6,%ymm12,%ymm14 -vaddpd %ymm7,%ymm13,%ymm15 -vaddpd %ymm4,%ymm8,%ymm8 -vaddpd %ymm5,%ymm9,%ymm9 -vsubpd %ymm6,%ymm12,%ymm12 -vsubpd %ymm7,%ymm13,%ymm13 - -.third_pass: -vmovupd 32(%rsi),%xmm0 /* gamma */ -vmovupd 48(%rsi),%xmm2 /* delta */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 -vinsertf128 $1, %xmm2, %ymm2, %ymm2 -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vshufpd $5, %ymm9, %ymm9, %ymm4 -vshufpd $5, %ymm11, %ymm11, %ymm5 -vshufpd $5, %ymm13, %ymm13, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm0,%ymm5 -vmulpd %ymm6,%ymm3,%ymm6 -vmulpd %ymm7,%ymm2,%ymm7 -vfmaddsub231pd %ymm9, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm10) +/- ymm4 -vfmsubadd231pd %ymm11, %ymm1, %ymm5 -vfmaddsub231pd %ymm13, %ymm2, %ymm6 -vfmsubadd231pd %ymm15, %ymm3, %ymm7 -vsubpd %ymm4,%ymm8,%ymm9 -vaddpd %ymm5,%ymm10,%ymm11 -vsubpd %ymm6,%ymm12,%ymm13 -vaddpd %ymm7,%ymm14,%ymm15 -vaddpd %ymm4,%ymm8,%ymm8 -vsubpd %ymm5,%ymm10,%ymm10 -vaddpd %ymm6,%ymm12,%ymm12 -vsubpd %ymm7,%ymm14,%ymm14 - -.fourth_pass: -vmovupd 64(%rsi),%ymm0 /* gamma */ -vmovupd 96(%rsi),%ymm2 /* delta */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vperm2f128 $0x31,%ymm10,%ymm8,%ymm4 # ymm4 contains c1,c5 -- x gamma -vperm2f128 $0x31,%ymm11,%ymm9,%ymm5 # ymm5 contains c3,c7 -- x igamma -vperm2f128 $0x31,%ymm14,%ymm12,%ymm6 # ymm6 contains c9,c13 -- x delta -vperm2f128 $0x31,%ymm15,%ymm13,%ymm7 # ymm7 contains c11,c15 -- x idelta -vperm2f128 $0x20,%ymm10,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm11,%ymm9,%ymm9 # ymm9 contains c2,c6 -vperm2f128 $0x20,%ymm14,%ymm12,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x20,%ymm15,%ymm13,%ymm11 # ymm11 contains c10,c14 -vshufpd $5, %ymm4, %ymm4, %ymm12 -vshufpd $5, %ymm5, %ymm5, %ymm13 -vshufpd $5, %ymm6, %ymm6, %ymm14 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm12,%ymm1,%ymm12 -vmulpd %ymm13,%ymm0,%ymm13 -vmulpd %ymm14,%ymm3,%ymm14 -vmulpd %ymm15,%ymm2,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm12 # ymm12 = (ymm0 * ymm4) +/- ymm12 -vfmsubadd231pd %ymm5, %ymm1, %ymm13 -vfmaddsub231pd %ymm6, %ymm2, %ymm14 -vfmsubadd231pd %ymm7, %ymm3, %ymm15 -vsubpd %ymm12,%ymm8,%ymm4 -vaddpd %ymm13,%ymm9,%ymm5 -vsubpd %ymm14,%ymm10,%ymm6 -vaddpd %ymm15,%ymm11,%ymm7 -vaddpd %ymm12,%ymm8,%ymm8 -vsubpd %ymm13,%ymm9,%ymm9 -vaddpd %ymm14,%ymm10,%ymm10 -vsubpd %ymm15,%ymm11,%ymm11 - -vperm2f128 $0x20,%ymm6,%ymm10,%ymm12 # ymm4 contains c1,c5 -- x gamma -vperm2f128 $0x20,%ymm7,%ymm11,%ymm13 # ymm5 contains c3,c7 -- x igamma -vperm2f128 $0x31,%ymm6,%ymm10,%ymm14 # ymm6 contains c9,c13 -- x delta -vperm2f128 $0x31,%ymm7,%ymm11,%ymm15 # ymm7 contains c11,c15 -- x idelta -vperm2f128 $0x31,%ymm4,%ymm8,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x31,%ymm5,%ymm9,%ymm11 # ymm11 contains c10,c14 -vperm2f128 $0x20,%ymm4,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm5,%ymm9,%ymm9 # ymm9 contains c2,c6 - -.save_and_return: -vmovupd %ymm8,(%rdi) -vmovupd %ymm9,0x20(%rdi) -vmovupd %ymm10,0x40(%rdi) -vmovupd %ymm11,0x60(%rdi) -vmovupd %ymm12,0x80(%rdi) -vmovupd %ymm13,0xa0(%rdi) -vmovupd %ymm14,0xc0(%rdi) -vmovupd %ymm15,0xe0(%rdi) -ret
@@ -1,8 +0,0 @@
-#include "../commons_private.h" -#include "cplx_fft_private.h" - -__always_inline void my_asserts() { - STATIC_ASSERT(sizeof(FFT_FUNCTION) == 8); - STATIC_ASSERT(sizeof(CPLX_FFT_PRECOMP) == 40); - STATIC_ASSERT(sizeof(CPLX_IFFT_PRECOMP) == 40); -}
@@ -1,266 +0,0 @@
-#include <immintrin.h> -#include <string.h> - -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -typedef double D4MEM[4]; - -/** - * @brief complex fft via bfs strategy (for m between 2 and 8) - * @param dat the data to run the algorithm on - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -void cplx_fft_avx2_fma_bfs_2(D4MEM* dat, const D4MEM** omg, uint32_t m) { - double* data = (double*)dat; - int32_t _2nblock = m >> 1; // = h in ref code - D4MEM* const finaldd = (D4MEM*)(data + 2 * m); - while (_2nblock >= 2) { - int32_t nblock = _2nblock >> 1; // =h/2 in ref code - D4MEM* dd = (D4MEM*)data; - do { - const __m256d om = _mm256_load_pd(*omg[0]); - const __m256d omim = _mm256_addsub_pd(_mm256_setzero_pd(), _mm256_unpackhi_pd(om, om)); - const __m256d omre = _mm256_unpacklo_pd(om, om); - D4MEM* const ddend = (dd + nblock); - D4MEM* ddmid = ddend; - do { - const __m256d b = _mm256_loadu_pd(ddmid[0]); - const __m256d t1 = _mm256_mul_pd(b, omre); - const __m256d barb = _mm256_shuffle_pd(b, b, 5); - const __m256d t2 = _mm256_fmadd_pd(barb, omim, t1); - const __m256d a = _mm256_loadu_pd(dd[0]); - const __m256d newa = _mm256_add_pd(a, t2); - const __m256d newb = _mm256_sub_pd(a, t2); - _mm256_storeu_pd(dd[0], newa); - _mm256_storeu_pd(ddmid[0], newb); - dd += 1; - ddmid += 1; - } while (dd < ddend); - dd += nblock; - *omg += 1; - } while (dd < finaldd); - _2nblock >>= 1; - } - // last iteration when _2nblock == 1 - { - D4MEM* dd = (D4MEM*)data; - do { - const __m256d om = _mm256_load_pd(*omg[0]); - const __m256d omre = _mm256_unpacklo_pd(om, om); - const __m256d omim = _mm256_unpackhi_pd(om, om); - const __m256d ab = _mm256_loadu_pd(dd[0]); - const __m256d bb = _mm256_permute4x64_pd(ab, 0b11101110); - const __m256d bbbar = _mm256_permute4x64_pd(ab, 0b10111011); - const __m256d t1 = _mm256_mul_pd(bbbar, omim); - const __m256d t2 = _mm256_fmaddsub_pd(bb, omre, t1); - const __m256d aa = _mm256_permute4x64_pd(ab, 0b01000100); - const __m256d newab = _mm256_add_pd(aa, t2); - _mm256_storeu_pd(dd[0], newab); - dd += 1; - *omg += 1; - } while (dd < finaldd); - } -} - -__always_inline void cplx_twiddle_fft_avx2(int32_t h, D4MEM* data, const void* omg) { - const __m256d om = _mm256_loadu_pd(omg); - const __m256d omim = _mm256_unpackhi_pd(om, om); - const __m256d omre = _mm256_unpacklo_pd(om, om); - D4MEM* d0 = data; - D4MEM* const ddend = d0 + (h >> 1); - D4MEM* d1 = ddend; - do { - const __m256d b = _mm256_loadu_pd(d1[0]); - const __m256d barb = _mm256_shuffle_pd(b, b, 5); - const __m256d t1 = _mm256_mul_pd(barb, omim); - const __m256d t2 = _mm256_fmaddsub_pd(b, omre, t1); - const __m256d a = _mm256_loadu_pd(d0[0]); - const __m256d newa = _mm256_add_pd(a, t2); - const __m256d newb = _mm256_sub_pd(a, t2); - _mm256_storeu_pd(d0[0], newa); - _mm256_storeu_pd(d1[0], newb); - d0 += 1; - d1 += 1; - } while (d0 < ddend); -} - -__always_inline void cplx_bitwiddle_fft_avx2(int32_t h, void* data, const void* powom) { - const __m256d omx = _mm256_loadu_pd(powom); - const __m256d oma = _mm256_permute2f128_pd(omx, omx, 0x00); - const __m256d omb = _mm256_permute2f128_pd(omx, omx, 0x11); - const __m256d omaim = _mm256_unpackhi_pd(oma, oma); - const __m256d omare = _mm256_unpacklo_pd(oma, oma); - const __m256d ombim = _mm256_unpackhi_pd(omb, omb); - const __m256d ombre = _mm256_unpacklo_pd(omb, omb); - D4MEM* d0 = (D4MEM*)data; - D4MEM* const ddend = d0 + (h >> 1); - D4MEM* d1 = ddend; - D4MEM* d2 = d0 + h; - D4MEM* d3 = d1 + h; - __m256d reg0, reg1, reg2, reg3, tmp0, tmp1; - do { - reg0 = _mm256_loadu_pd(d0[0]); - reg1 = _mm256_loadu_pd(d1[0]); - reg2 = _mm256_loadu_pd(d2[0]); - reg3 = _mm256_loadu_pd(d3[0]); - tmp0 = _mm256_shuffle_pd(reg2, reg2, 5); - tmp1 = _mm256_shuffle_pd(reg3, reg3, 5); - tmp0 = _mm256_mul_pd(tmp0, omaim); - tmp1 = _mm256_mul_pd(tmp1, omaim); - tmp0 = _mm256_fmaddsub_pd(reg2, omare, tmp0); - tmp1 = _mm256_fmaddsub_pd(reg3, omare, tmp1); - reg2 = _mm256_sub_pd(reg0, tmp0); - reg3 = _mm256_sub_pd(reg1, tmp1); - reg0 = _mm256_add_pd(reg0, tmp0); - reg1 = _mm256_add_pd(reg1, tmp1); - //-------------------------------------- - tmp0 = _mm256_shuffle_pd(reg1, reg1, 5); - tmp1 = _mm256_shuffle_pd(reg3, reg3, 5); - tmp0 = _mm256_mul_pd(tmp0, ombim); //(r,i) - tmp1 = _mm256_mul_pd(tmp1, ombre); //(-i,r) - tmp0 = _mm256_fmaddsub_pd(reg1, ombre, tmp0); - tmp1 = _mm256_fmsubadd_pd(reg3, ombim, tmp1); - reg1 = _mm256_sub_pd(reg0, tmp0); - reg3 = _mm256_add_pd(reg2, tmp1); - reg0 = _mm256_add_pd(reg0, tmp0); - reg2 = _mm256_sub_pd(reg2, tmp1); - ///// - _mm256_storeu_pd(d0[0], reg0); - _mm256_storeu_pd(d1[0], reg1); - _mm256_storeu_pd(d2[0], reg2); - _mm256_storeu_pd(d3[0], reg3); - d0 += 1; - d1 += 1; - d2 += 1; - d3 += 1; - } while (d0 < ddend); -} - -/** - * @brief complex fft via bfs strategy (for m >= 16) - * @param dat the data to run the algorithm on - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -void cplx_fft_avx2_fma_bfs_16(D4MEM* dat, const D4MEM** omg, uint32_t m) { - double* data = (double*)dat; - D4MEM* const finaldd = (D4MEM*)(data + 2 * m); - uint32_t mm = m; - uint32_t log2m = _mm_popcnt_u32(m - 1); // log2(m) - if (log2m % 2 == 1) { - uint32_t h = mm >> 1; - cplx_twiddle_fft_avx2(h, dat, **omg); - *omg += 1; - mm >>= 1; - } - while (mm > 16) { - uint32_t h = mm / 4; - for (CPLX* d = (CPLX*)data; d < (CPLX*)finaldd; d += mm) { - cplx_bitwiddle_fft_avx2(h, d, (CPLX*)*omg); - *omg += 1; - } - mm = h; - } - { - D4MEM* dd = (D4MEM*)data; - do { - cplx_fft16_avx_fma(dd, *omg); - dd += 8; - *omg += 4; - } while (dd < finaldd); - _mm256_zeroupper(); - } - /* - int32_t _2nblock = m >> 1; // = h in ref code - while (_2nblock >= 16) { - int32_t nblock = _2nblock >> 1; // =h/2 in ref code - D4MEM* dd = (D4MEM*)data; - do { - const __m256d om = _mm256_load_pd(*omg[0]); - const __m256d omim = _mm256_addsub_pd(_mm256_setzero_pd(), _mm256_unpackhi_pd(om, om)); - const __m256d omre = _mm256_unpacklo_pd(om, om); - D4MEM* const ddend = (dd + nblock); - D4MEM* ddmid = ddend; - do { - const __m256d b = _mm256_loadu_pd(ddmid[0]); - const __m256d t1 = _mm256_mul_pd(b, omre); - const __m256d barb = _mm256_shuffle_pd(b, b, 5); - const __m256d t2 = _mm256_fmadd_pd(barb, omim, t1); - const __m256d a = _mm256_loadu_pd(dd[0]); - const __m256d newa = _mm256_add_pd(a, t2); - const __m256d newb = _mm256_sub_pd(a, t2); - _mm256_storeu_pd(dd[0], newa); - _mm256_storeu_pd(ddmid[0], newb); - dd += 1; - ddmid += 1; - } while (dd < ddend); - dd += nblock; - *omg += 1; - } while (dd < finaldd); - _2nblock >>= 1; - } - // last iteration when _2nblock == 8 - { - D4MEM* dd = (D4MEM*)data; - do { - cplx_fft16_avx_fma(dd, *omg); - dd += 8; - *omg += 4; - } while (dd < finaldd); - _mm256_zeroupper(); - } - */ -} - -/** - * @brief complex fft via dfs recursion (for m >= 16) - * @param dat the data to run the algorithm on - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -void cplx_fft_avx2_fma_rec_16(D4MEM* dat, const D4MEM** omg, uint32_t m) { - if (m <= 8) return cplx_fft_avx2_fma_bfs_2(dat, omg, m); - if (m <= 2048) return cplx_fft_avx2_fma_bfs_16(dat, omg, m); - double* data = (double*)dat; - int32_t _2nblock = m >> 1; // = h in ref code - int32_t nblock = _2nblock >> 1; // =h/2 in ref code - D4MEM* dd = (D4MEM*)data; - const __m256d om = _mm256_load_pd(*omg[0]); - const __m256d omim = _mm256_addsub_pd(_mm256_setzero_pd(), _mm256_unpackhi_pd(om, om)); - const __m256d omre = _mm256_unpacklo_pd(om, om); - D4MEM* const ddend = (dd + nblock); - D4MEM* ddmid = ddend; - do { - const __m256d b = _mm256_loadu_pd(ddmid[0]); - const __m256d t1 = _mm256_mul_pd(b, omre); - const __m256d barb = _mm256_shuffle_pd(b, b, 5); - const __m256d t2 = _mm256_fmadd_pd(barb, omim, t1); - const __m256d a = _mm256_loadu_pd(dd[0]); - const __m256d newa = _mm256_add_pd(a, t2); - const __m256d newb = _mm256_sub_pd(a, t2); - _mm256_storeu_pd(dd[0], newa); - _mm256_storeu_pd(ddmid[0], newb); - dd += 1; - ddmid += 1; - } while (dd < ddend); - *omg += 1; - cplx_fft_avx2_fma_rec_16(dat, omg, _2nblock); - cplx_fft_avx2_fma_rec_16(ddend, omg, _2nblock); -} - -/** - * @brief complex fft via best strategy (for m>=1) - * @param dat the data to run the algorithm on: m complex numbers - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -EXPORT void cplx_fft_avx2_fma(const CPLX_FFT_PRECOMP* precomp, void* d) { - const uint32_t m = precomp->m; - const D4MEM* omg = (D4MEM*)precomp->powomegas; - if (m <= 1) return; - if (m <= 8) return cplx_fft_avx2_fma_bfs_2(d, &omg, m); - if (m <= 2048) return cplx_fft_avx2_fma_bfs_16(d, &omg, m); - cplx_fft_avx2_fma_rec_16(d, &omg, m); -}
@@ -1,451 +0,0 @@
-#include <immintrin.h> - -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -typedef double D2MEM[2]; -typedef double D4MEM[4]; -typedef double D8MEM[8]; - -EXPORT void cplx_fftvec_addmul_avx512(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, - const void* b) { - const uint32_t m = precomp->m; - const D8MEM* aa = (D8MEM*)a; - const D8MEM* bb = (D8MEM*)b; - D8MEM* rr = (D8MEM*)r; - const D8MEM* const aend = aa + (m >> 2); - do { - /* -BEGIN_TEMPLATE -const __m512d ari% = _mm512_loadu_pd(aa[%]); -const __m512d bri% = _mm512_loadu_pd(bb[%]); -const __m512d rri% = _mm512_loadu_pd(rr[%]); -const __m512d bir% = _mm512_shuffle_pd(bri%,bri%, 0b01010101); -const __m512d aii% = _mm512_shuffle_pd(ari%,ari%, 0b11111111); -const __m512d pro% = _mm512_fmaddsub_pd(aii%,bir%,rri%); -const __m512d arr% = _mm512_shuffle_pd(ari%,ari%, 0b00000000); -const __m512d res% = _mm512_fmaddsub_pd(arr%,bri%,pro%); -_mm512_storeu_pd(rr[%],res%); -rr += @; // ONCE -aa += @; // ONCE -bb += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 2 - const __m512d ari0 = _mm512_loadu_pd(aa[0]); - const __m512d ari1 = _mm512_loadu_pd(aa[1]); - const __m512d bri0 = _mm512_loadu_pd(bb[0]); - const __m512d bri1 = _mm512_loadu_pd(bb[1]); - const __m512d rri0 = _mm512_loadu_pd(rr[0]); - const __m512d rri1 = _mm512_loadu_pd(rr[1]); - const __m512d bir0 = _mm512_shuffle_pd(bri0, bri0, 0b01010101); - const __m512d bir1 = _mm512_shuffle_pd(bri1, bri1, 0b01010101); - const __m512d aii0 = _mm512_shuffle_pd(ari0, ari0, 0b11111111); - const __m512d aii1 = _mm512_shuffle_pd(ari1, ari1, 0b11111111); - const __m512d pro0 = _mm512_fmaddsub_pd(aii0, bir0, rri0); - const __m512d pro1 = _mm512_fmaddsub_pd(aii1, bir1, rri1); - const __m512d arr0 = _mm512_shuffle_pd(ari0, ari0, 0b00000000); - const __m512d arr1 = _mm512_shuffle_pd(ari1, ari1, 0b00000000); - const __m512d res0 = _mm512_fmaddsub_pd(arr0, bri0, pro0); - const __m512d res1 = _mm512_fmaddsub_pd(arr1, bri1, pro1); - _mm512_storeu_pd(rr[0], res0); - _mm512_storeu_pd(rr[1], res1); - rr += 2; // ONCE - aa += 2; // ONCE - bb += 2; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -#if 0 -EXPORT void cplx_fftvec_mul_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%, 5); // conj of b -const __m256d aii% = _mm256_shuffle_pd(ari%,ari%, 15); // im of a -const __m256d pro% = _mm256_mul_pd(aii%,bir%); -const __m256d arr% = _mm256_shuffle_pd(ari%,ari%, 0); // rr of a -const __m256d res% = _mm256_fmaddsub_pd(arr%,bri%,pro%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d bir0 = _mm256_shuffle_pd(bri0,bri0, 5); // conj of b -const __m256d bir1 = _mm256_shuffle_pd(bri1,bri1, 5); // conj of b -const __m256d bir2 = _mm256_shuffle_pd(bri2,bri2, 5); // conj of b -const __m256d bir3 = _mm256_shuffle_pd(bri3,bri3, 5); // conj of b -const __m256d aii0 = _mm256_shuffle_pd(ari0,ari0, 15); // im of a -const __m256d aii1 = _mm256_shuffle_pd(ari1,ari1, 15); // im of a -const __m256d aii2 = _mm256_shuffle_pd(ari2,ari2, 15); // im of a -const __m256d aii3 = _mm256_shuffle_pd(ari3,ari3, 15); // im of a -const __m256d pro0 = _mm256_mul_pd(aii0,bir0); -const __m256d pro1 = _mm256_mul_pd(aii1,bir1); -const __m256d pro2 = _mm256_mul_pd(aii2,bir2); -const __m256d pro3 = _mm256_mul_pd(aii3,bir3); -const __m256d arr0 = _mm256_shuffle_pd(ari0,ari0, 0); // rr of a -const __m256d arr1 = _mm256_shuffle_pd(ari1,ari1, 0); // rr of a -const __m256d arr2 = _mm256_shuffle_pd(ari2,ari2, 0); // rr of a -const __m256d arr3 = _mm256_shuffle_pd(ari3,ari3, 0); // rr of a -const __m256d res0 = _mm256_fmaddsub_pd(arr0,bri0,pro0); -const __m256d res1 = _mm256_fmaddsub_pd(arr1,bri1,pro1); -const __m256d res2 = _mm256_fmaddsub_pd(arr2,bri2,pro2); -const __m256d res3 = _mm256_fmaddsub_pd(arr3,bri3,pro3); -_mm256_storeu_pd(rr[0],res0); -_mm256_storeu_pd(rr[1],res1); -_mm256_storeu_pd(rr[2],res2); -_mm256_storeu_pd(rr[3],res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_add_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d res% = _mm256_add_pd(ari%,bri%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d res0 = _mm256_add_pd(ari0,bri0); -const __m256d res1 = _mm256_add_pd(ari1,bri1); -const __m256d res2 = _mm256_add_pd(ari2,bri2); -const __m256d res3 = _mm256_add_pd(ari3,bri3); -_mm256_storeu_pd(rr[0],res0); -_mm256_storeu_pd(rr[1],res1); -_mm256_storeu_pd(rr[2],res2); -_mm256_storeu_pd(rr[3],res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_sub2_to_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d sum% = _mm256_add_pd(ari%,bri%); -const __m256d rri% = _mm256_loadu_pd(rr[%]); -const __m256d res% = _mm256_sub_pd(rri%,sum%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d sum0 = _mm256_add_pd(ari0,bri0); -const __m256d sum1 = _mm256_add_pd(ari1,bri1); -const __m256d sum2 = _mm256_add_pd(ari2,bri2); -const __m256d sum3 = _mm256_add_pd(ari3,bri3); -const __m256d rri0 = _mm256_loadu_pd(rr[0]); -const __m256d rri1 = _mm256_loadu_pd(rr[1]); -const __m256d rri2 = _mm256_loadu_pd(rr[2]); -const __m256d rri3 = _mm256_loadu_pd(rr[3]); -const __m256d res0 = _mm256_sub_pd(rri0,sum0); -const __m256d res1 = _mm256_sub_pd(rri1,sum1); -const __m256d res2 = _mm256_sub_pd(rri2,sum2); -const __m256d res3 = _mm256_sub_pd(rri3,sum3); -_mm256_storeu_pd(rr[0],res0); -_mm256_storeu_pd(rr[1],res1); -_mm256_storeu_pd(rr[2],res2); -_mm256_storeu_pd(rr[3],res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_copy_fma(uint32_t m, void* r, const void* a) { - const double(*aa)[4] = (double(*)[4])a; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -_mm256_storeu_pd(rr[%],ari%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -_mm256_storeu_pd(rr[0],ari0); -_mm256_storeu_pd(rr[1],ari1); -_mm256_storeu_pd(rr[2],ari2); -_mm256_storeu_pd(rr[3],ari3); - // END_INTERLEAVE - rr += 4; - aa += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_twiddle_fma(uint32_t m, void* a, void* b, const void* omg) { - double(*aa)[4] = (double(*)[4])a; - double(*bb)[4] = (double(*)[4])b; - const double(*const aend)[4] = aa + (m >> 1); - const __m256d om = _mm256_loadu_pd(omg); - const __m256d omrr = _mm256_shuffle_pd(om, om, 0); - const __m256d omii = _mm256_shuffle_pd(om, om, 15); - do { - /* -BEGIN_TEMPLATE -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%,5); -__m256d p% = _mm256_mul_pd(bir%,omii); -p% = _mm256_fmaddsub_pd(bri%,omrr,p%); -const __m256d ari% = _mm256_loadu_pd(aa[%]); -_mm256_storeu_pd(aa[%],_mm256_add_pd(ari%,p%)); -_mm256_storeu_pd(bb[%],_mm256_sub_pd(ari%,p%)); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d bir0 = _mm256_shuffle_pd(bri0,bri0,5); -const __m256d bir1 = _mm256_shuffle_pd(bri1,bri1,5); -const __m256d bir2 = _mm256_shuffle_pd(bri2,bri2,5); -const __m256d bir3 = _mm256_shuffle_pd(bri3,bri3,5); -__m256d p0 = _mm256_mul_pd(bir0,omii); -__m256d p1 = _mm256_mul_pd(bir1,omii); -__m256d p2 = _mm256_mul_pd(bir2,omii); -__m256d p3 = _mm256_mul_pd(bir3,omii); -p0 = _mm256_fmaddsub_pd(bri0,omrr,p0); -p1 = _mm256_fmaddsub_pd(bri1,omrr,p1); -p2 = _mm256_fmaddsub_pd(bri2,omrr,p2); -p3 = _mm256_fmaddsub_pd(bri3,omrr,p3); -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -_mm256_storeu_pd(aa[0],_mm256_add_pd(ari0,p0)); -_mm256_storeu_pd(aa[1],_mm256_add_pd(ari1,p1)); -_mm256_storeu_pd(aa[2],_mm256_add_pd(ari2,p2)); -_mm256_storeu_pd(aa[3],_mm256_add_pd(ari3,p3)); -_mm256_storeu_pd(bb[0],_mm256_sub_pd(ari0,p0)); -_mm256_storeu_pd(bb[1],_mm256_sub_pd(ari1,p1)); -_mm256_storeu_pd(bb[2],_mm256_sub_pd(ari2,p2)); -_mm256_storeu_pd(bb[3],_mm256_sub_pd(ari3,p3)); - // END_INTERLEAVE - bb += 4; - aa += 4; - } while (aa < aend); -} -#endif - -EXPORT void cplx_fftvec_twiddle_avx512(const CPLX_FFTVEC_TWIDDLE_PRECOMP* precomp, void* a, void* b, const void* omg) { - const uint32_t m = precomp->m; - D8MEM* aa = (D8MEM*)a; - D8MEM* bb = (D8MEM*)b; - D8MEM* const aend = aa + (m >> 2); - const __m512d om = _mm512_broadcast_f64x4(_mm256_loadu_pd(omg)); - const __m512d omrr = _mm512_shuffle_pd(om, om, 0b00000000); - const __m512d omii = _mm512_shuffle_pd(om, om, 0b11111111); - do { - /* - BEGIN_TEMPLATE - const __m512d bri% = _mm512_loadu_pd(bb[%]); - const __m512d bir% = _mm512_shuffle_pd(bri%,bri%,0b10011001); - __m512d p% = _mm512_mul_pd(bir%,omii); - p% = _mm512_fmaddsub_pd(bri%,omrr,p%); - const __m512d ari% = _mm512_loadu_pd(aa[%]); - _mm512_storeu_pd(aa[%],_mm512_add_pd(ari%,p%)); - _mm512_storeu_pd(bb[%],_mm512_sub_pd(ari%,p%)); - bb += @; // ONCE - aa += @; // ONCE - END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 - const __m512d bri0 = _mm512_loadu_pd(bb[0]); - const __m512d bri1 = _mm512_loadu_pd(bb[1]); - const __m512d bri2 = _mm512_loadu_pd(bb[2]); - const __m512d bri3 = _mm512_loadu_pd(bb[3]); - const __m512d bir0 = _mm512_shuffle_pd(bri0, bri0, 0b10011001); - const __m512d bir1 = _mm512_shuffle_pd(bri1, bri1, 0b10011001); - const __m512d bir2 = _mm512_shuffle_pd(bri2, bri2, 0b10011001); - const __m512d bir3 = _mm512_shuffle_pd(bri3, bri3, 0b10011001); - __m512d p0 = _mm512_mul_pd(bir0, omii); - __m512d p1 = _mm512_mul_pd(bir1, omii); - __m512d p2 = _mm512_mul_pd(bir2, omii); - __m512d p3 = _mm512_mul_pd(bir3, omii); - p0 = _mm512_fmaddsub_pd(bri0, omrr, p0); - p1 = _mm512_fmaddsub_pd(bri1, omrr, p1); - p2 = _mm512_fmaddsub_pd(bri2, omrr, p2); - p3 = _mm512_fmaddsub_pd(bri3, omrr, p3); - const __m512d ari0 = _mm512_loadu_pd(aa[0]); - const __m512d ari1 = _mm512_loadu_pd(aa[1]); - const __m512d ari2 = _mm512_loadu_pd(aa[2]); - const __m512d ari3 = _mm512_loadu_pd(aa[3]); - _mm512_storeu_pd(aa[0], _mm512_add_pd(ari0, p0)); - _mm512_storeu_pd(aa[1], _mm512_add_pd(ari1, p1)); - _mm512_storeu_pd(aa[2], _mm512_add_pd(ari2, p2)); - _mm512_storeu_pd(aa[3], _mm512_add_pd(ari3, p3)); - _mm512_storeu_pd(bb[0], _mm512_sub_pd(ari0, p0)); - _mm512_storeu_pd(bb[1], _mm512_sub_pd(ari1, p1)); - _mm512_storeu_pd(bb[2], _mm512_sub_pd(ari2, p2)); - _mm512_storeu_pd(bb[3], _mm512_sub_pd(ari3, p3)); - bb += 4; // ONCE - aa += 4; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -EXPORT void cplx_fftvec_bitwiddle_avx512(const CPLX_FFTVEC_BITWIDDLE_PRECOMP* precomp, void* a, uint64_t slicea, - const void* omg) { - const uint32_t m = precomp->m; - const uint64_t OFFSET = slicea / sizeof(D8MEM); - D8MEM* aa = (D8MEM*)a; - const D8MEM* aend = aa + (m >> 2); - const __m512d om = _mm512_broadcast_f64x4(_mm256_loadu_pd(omg)); - const __m512d om1rr = _mm512_shuffle_pd(om, om, 0); - const __m512d om1ii = _mm512_shuffle_pd(om, om, 15); - const __m512d om2rr = _mm512_shuffle_pd(om, om, 0); - const __m512d om2ii = _mm512_shuffle_pd(om, om, 0); - const __m512d om3rr = _mm512_shuffle_pd(om, om, 15); - const __m512d om3ii = _mm512_shuffle_pd(om, om, 15); - do { - /* - BEGIN_TEMPLATE - __m512d ari% = _mm512_loadu_pd(aa[%]); - __m512d bri% = _mm512_loadu_pd((aa+OFFSET)[%]); - __m512d cri% = _mm512_loadu_pd((aa+2*OFFSET)[%]); - __m512d dri% = _mm512_loadu_pd((aa+3*OFFSET)[%]); - __m512d pa% = _mm512_shuffle_pd(cri%,cri%,5); - __m512d pb% = _mm512_shuffle_pd(dri%,dri%,5); - pa% = _mm512_mul_pd(pa%,om1ii); - pb% = _mm512_mul_pd(pb%,om1ii); - pa% = _mm512_fmaddsub_pd(cri%,om1rr,pa%); - pb% = _mm512_fmaddsub_pd(dri%,om1rr,pb%); - cri% = _mm512_sub_pd(ari%,pa%); - dri% = _mm512_sub_pd(bri%,pb%); - ari% = _mm512_add_pd(ari%,pa%); - bri% = _mm512_add_pd(bri%,pb%); - pa% = _mm512_shuffle_pd(bri%,bri%,5); - pb% = _mm512_shuffle_pd(dri%,dri%,5); - pa% = _mm512_mul_pd(pa%,om2ii); - pb% = _mm512_mul_pd(pb%,om3ii); - pa% = _mm512_fmaddsub_pd(bri%,om2rr,pa%); - pb% = _mm512_fmaddsub_pd(dri%,om3rr,pb%); - bri% = _mm512_sub_pd(ari%,pa%); - dri% = _mm512_sub_pd(cri%,pb%); - ari% = _mm512_add_pd(ari%,pa%); - cri% = _mm512_add_pd(cri%,pb%); - _mm512_storeu_pd(aa[%], ari%); - _mm512_storeu_pd((aa+OFFSET)[%],bri%); - _mm512_storeu_pd((aa+2*OFFSET)[%],cri%); - _mm512_storeu_pd((aa+3*OFFSET)[%],dri%); - aa += @; // ONCE - END_TEMPLATE - */ - // BEGIN_INTERLEAVE 2 - __m512d ari0 = _mm512_loadu_pd(aa[0]); - __m512d ari1 = _mm512_loadu_pd(aa[1]); - __m512d bri0 = _mm512_loadu_pd((aa + OFFSET)[0]); - __m512d bri1 = _mm512_loadu_pd((aa + OFFSET)[1]); - __m512d cri0 = _mm512_loadu_pd((aa + 2 * OFFSET)[0]); - __m512d cri1 = _mm512_loadu_pd((aa + 2 * OFFSET)[1]); - __m512d dri0 = _mm512_loadu_pd((aa + 3 * OFFSET)[0]); - __m512d dri1 = _mm512_loadu_pd((aa + 3 * OFFSET)[1]); - __m512d pa0 = _mm512_shuffle_pd(cri0, cri0, 5); - __m512d pa1 = _mm512_shuffle_pd(cri1, cri1, 5); - __m512d pb0 = _mm512_shuffle_pd(dri0, dri0, 5); - __m512d pb1 = _mm512_shuffle_pd(dri1, dri1, 5); - pa0 = _mm512_mul_pd(pa0, om1ii); - pa1 = _mm512_mul_pd(pa1, om1ii); - pb0 = _mm512_mul_pd(pb0, om1ii); - pb1 = _mm512_mul_pd(pb1, om1ii); - pa0 = _mm512_fmaddsub_pd(cri0, om1rr, pa0); - pa1 = _mm512_fmaddsub_pd(cri1, om1rr, pa1); - pb0 = _mm512_fmaddsub_pd(dri0, om1rr, pb0); - pb1 = _mm512_fmaddsub_pd(dri1, om1rr, pb1); - cri0 = _mm512_sub_pd(ari0, pa0); - cri1 = _mm512_sub_pd(ari1, pa1); - dri0 = _mm512_sub_pd(bri0, pb0); - dri1 = _mm512_sub_pd(bri1, pb1); - ari0 = _mm512_add_pd(ari0, pa0); - ari1 = _mm512_add_pd(ari1, pa1); - bri0 = _mm512_add_pd(bri0, pb0); - bri1 = _mm512_add_pd(bri1, pb1); - pa0 = _mm512_shuffle_pd(bri0, bri0, 5); - pa1 = _mm512_shuffle_pd(bri1, bri1, 5); - pb0 = _mm512_shuffle_pd(dri0, dri0, 5); - pb1 = _mm512_shuffle_pd(dri1, dri1, 5); - pa0 = _mm512_mul_pd(pa0, om2ii); - pa1 = _mm512_mul_pd(pa1, om2ii); - pb0 = _mm512_mul_pd(pb0, om3ii); - pb1 = _mm512_mul_pd(pb1, om3ii); - pa0 = _mm512_fmaddsub_pd(bri0, om2rr, pa0); - pa1 = _mm512_fmaddsub_pd(bri1, om2rr, pa1); - pb0 = _mm512_fmaddsub_pd(dri0, om3rr, pb0); - pb1 = _mm512_fmaddsub_pd(dri1, om3rr, pb1); - bri0 = _mm512_sub_pd(ari0, pa0); - bri1 = _mm512_sub_pd(ari1, pa1); - dri0 = _mm512_sub_pd(cri0, pb0); - dri1 = _mm512_sub_pd(cri1, pb1); - ari0 = _mm512_add_pd(ari0, pa0); - ari1 = _mm512_add_pd(ari1, pa1); - cri0 = _mm512_add_pd(cri0, pb0); - cri1 = _mm512_add_pd(cri1, pb1); - _mm512_storeu_pd(aa[0], ari0); - _mm512_storeu_pd(aa[1], ari1); - _mm512_storeu_pd((aa + OFFSET)[0], bri0); - _mm512_storeu_pd((aa + OFFSET)[1], bri1); - _mm512_storeu_pd((aa + 2 * OFFSET)[0], cri0); - _mm512_storeu_pd((aa + 2 * OFFSET)[1], cri1); - _mm512_storeu_pd((aa + 3 * OFFSET)[0], dri0); - _mm512_storeu_pd((aa + 3 * OFFSET)[1], dri1); - aa += 2; // ONCE - // END_INTERLEAVE - } while (aa < aend); - _mm256_zeroupper(); -}
@@ -1,123 +0,0 @@
-#ifndef SPQLIOS_CPLX_FFT_INTERNAL_H -#define SPQLIOS_CPLX_FFT_INTERNAL_H - -#include "cplx_fft.h" - -/** @brief a complex number contains two doubles real,imag */ -typedef double CPLX[2]; - -EXPORT void cplx_set(CPLX r, const CPLX a); -EXPORT void cplx_neg(CPLX r, const CPLX a); -EXPORT void cplx_add(CPLX r, const CPLX a, const CPLX b); -EXPORT void cplx_sub(CPLX r, const CPLX a, const CPLX b); -EXPORT void cplx_mul(CPLX r, const CPLX a, const CPLX b); - -/** - * @brief splits 2h evaluations of one polynomials into 2 times h evaluations of even/odd polynomial - * Input: Q_0(y),...,Q_{h-1}(y),Q_0(-y),...,Q_{h-1}(-y) - * Output: P_0(z),...,P_{h-1}(z),P_h(z),...,P_{2h-1}(z) - * where Q_i(X)=P_i(X^2)+X.P_{h+i}(X^2) and y^2 = z - * @param h number of "coefficients" h >= 1 - * @param data 2h complex coefficients interleaved and 256b aligned - * @param powom y represented as (yre,yim) - */ -EXPORT void cplx_split_fft_ref(int32_t h, CPLX* data, const CPLX powom); -EXPORT void cplx_bisplit_fft_ref(int32_t h, CPLX* data, const CPLX powom[2]); - -/** - * Input: Q(y),Q(-y) - * Output: P_0(z),P_1(z) - * where Q(X)=P_0(X^2)+X.P_1(X^2) and y^2 = z - * @param data 2 complexes coefficients interleaved and 256b aligned - * @param powom (z,-z) interleaved: (zre,zim,-zre,-zim) - */ -EXPORT void split_fft_last_ref(CPLX* data, const CPLX powom); - -EXPORT void cplx_ifft_naive(const uint32_t m, const double entry_pwr, CPLX* data); -EXPORT void cplx_ifft16_avx_fma(void* data, const void* omega); -EXPORT void cplx_ifft16_ref(void* data, const void* omega); - -/** - * @brief compute the ifft evaluations of P in place - * ifft(data) = ifft_rec(data, i); - * function ifft_rec(data, omega) { - * if #data = 1: return data - * let s = sqrt(omega) w. re(s)>0 - * let (u,v) = data - * return split_fft([ifft_rec(u, s), ifft_rec(v, -s)],s) - * } - * @param itables precomputed tables (contains all the powers of omega in the order they are used) - * @param data vector of m complexes (coeffs as input, evals as output) - */ -EXPORT void cplx_ifft_ref(const CPLX_IFFT_PRECOMP* itables, void* data); -EXPORT void cplx_ifft_avx2_fma(const CPLX_IFFT_PRECOMP* itables, void* data); -EXPORT void cplx_fft_naive(const uint32_t m, const double entry_pwr, CPLX* data); -EXPORT void cplx_fft16_avx_fma(void* data, const void* omega); -EXPORT void cplx_fft16_ref(void* data, const void* omega); - -/** - * @brief compute the fft evaluations of P in place - * fft(data) = fft_rec(data, i); - * function fft_rec(data, omega) { - * if #data = 1: return data - * let s = sqrt(omega) w. re(s)>0 - * let (u,v) = merge_fft(data, s) - * return [fft_rec(u, s), fft_rec(v, -s)] - * } - * @param tables precomputed tables (contains all the powers of omega in the order they are used) - * @param data vector of m complexes (coeffs as input, evals as output) - */ -EXPORT void cplx_fft_ref(const CPLX_FFT_PRECOMP* tables, void* data); -EXPORT void cplx_fft_avx2_fma(const CPLX_FFT_PRECOMP* tables, void* data); - -/** - * @brief merges 2 times h evaluations of even/odd polynomials into 2h evaluations of a sigle polynomial - * Input: P_0(z),...,P_{h-1}(z),P_h(z),...,P_{2h-1}(z) - * Output: Q_0(y),...,Q_{h-1}(y),Q_0(-y),...,Q_{h-1}(-y) - * where Q_i(X)=P_i(X^2)+X.P_{h+i}(X^2) and y^2 = z - * @param h number of "coefficients" h >= 1 - * @param data 2h complex coefficients interleaved and 256b aligned - * @param powom y represented as (yre,yim) - */ -EXPORT void cplx_twiddle_fft_ref(int32_t h, CPLX* data, const CPLX powom); - -EXPORT void citwiddle(CPLX a, CPLX b, const CPLX om); -EXPORT void ctwiddle(CPLX a, CPLX b, const CPLX om); -EXPORT void invctwiddle(CPLX a, CPLX b, const CPLX ombar); -EXPORT void invcitwiddle(CPLX a, CPLX b, const CPLX ombar); - -// CONVERSIONS - -/** @brief r = x from ZnX (coeffs as signed int32_t's ) to double */ -EXPORT void cplx_from_znx32_ref(const CPLX_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x); -EXPORT void cplx_from_znx32_avx2_fma(const CPLX_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x); -/** @brief r = x to ZnX (coeffs as signed int32_t's ) to double */ -EXPORT void cplx_to_znx32_ref(const CPLX_TO_ZNX32_PRECOMP* precomp, int32_t* r, const void* x); -EXPORT void cplx_to_znx32_avx2_fma(const CPLX_TO_ZNX32_PRECOMP* precomp, int32_t* r, const void* x); -/** @brief r = x mod 1 from TnX (coeffs as signed int32_t's) to double */ -EXPORT void cplx_from_tnx32_ref(const CPLX_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x); -EXPORT void cplx_from_tnx32_avx2_fma(const CPLX_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x); -/** @brief r = x mod 1 from TnX (coeffs as signed int32_t's) */ -EXPORT void cplx_to_tnx32_ref(const CPLX_TO_TNX32_PRECOMP* precomp, int32_t* x, const void* c); -EXPORT void cplx_to_tnx32_avx2_fma(const CPLX_TO_TNX32_PRECOMP* precomp, int32_t* x, const void* c); -/** @brief r = x from RnX (coeffs as doubles ) to double */ -EXPORT void cplx_from_rnx64_ref(const CPLX_FROM_RNX64_PRECOMP* precomp, void* r, const double* x); -EXPORT void cplx_from_rnx64_avx2_fma(const CPLX_FROM_RNX64_PRECOMP* precomp, void* r, const double* x); -/** @brief r = x to RnX (coeffs as doubles ) to double */ -EXPORT void cplx_to_rnx64_ref(const CPLX_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -EXPORT void cplx_to_rnx64_avx2_fma(const CPLX_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -/** @brief r = x to integers in RnX (coeffs as doubles ) to double */ -EXPORT void cplx_round_to_rnx64_ref(const CPLX_ROUND_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -EXPORT void cplx_round_to_rnx64_avx2_fma(const CPLX_ROUND_TO_RNX64_PRECOMP* precomp, double* r, const void* x); - -// fftvec operations -/** @brief element-wise addmul r += ab */ -EXPORT void cplx_fftvec_addmul_ref(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b); -EXPORT void cplx_fftvec_addmul_fma(const CPLX_FFTVEC_ADDMUL_PRECOMP* tables, void* r, const void* a, const void* b); -EXPORT void cplx_fftvec_addmul_sse(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b); -EXPORT void cplx_fftvec_addmul_avx512(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b); -/** @brief element-wise mul r = ab */ -EXPORT void cplx_fftvec_mul_ref(const CPLX_FFTVEC_MUL_PRECOMP* tables, void* r, const void* a, const void* b); -EXPORT void cplx_fftvec_mul_fma(const CPLX_FFTVEC_MUL_PRECOMP* tables, void* r, const void* a, const void* b); - -#endif // SPQLIOS_CPLX_FFT_INTERNAL_H
@@ -1,109 +0,0 @@
-#ifndef SPQLIOS_CPLX_FFT_PRIVATE_H -#define SPQLIOS_CPLX_FFT_PRIVATE_H - -#include "cplx_fft.h" - -typedef struct cplx_twiddle_precomp CPLX_FFTVEC_TWIDDLE_PRECOMP; -typedef struct cplx_bitwiddle_precomp CPLX_FFTVEC_BITWIDDLE_PRECOMP; - -typedef void (*IFFT_FUNCTION)(const CPLX_IFFT_PRECOMP*, void*); -typedef void (*FFT_FUNCTION)(const CPLX_FFT_PRECOMP*, void*); -// conversions -typedef void (*FROM_ZNX32_FUNCTION)(const CPLX_FROM_ZNX32_PRECOMP*, void*, const int32_t*); -typedef void (*TO_ZNX32_FUNCTION)(const CPLX_FROM_ZNX32_PRECOMP*, int32_t*, const void*); -typedef void (*FROM_TNX32_FUNCTION)(const CPLX_FROM_TNX32_PRECOMP*, void*, const int32_t*); -typedef void (*TO_TNX32_FUNCTION)(const CPLX_TO_TNX32_PRECOMP*, int32_t*, const void*); -typedef void (*FROM_RNX64_FUNCTION)(const CPLX_FROM_RNX64_PRECOMP* precomp, void* r, const double* x); -typedef void (*TO_RNX64_FUNCTION)(const CPLX_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -typedef void (*ROUND_TO_RNX64_FUNCTION)(const CPLX_ROUND_TO_RNX64_PRECOMP* precomp, double* r, const void* x); -// fftvec operations -typedef void (*FFTVEC_MUL_FUNCTION)(const CPLX_FFTVEC_MUL_PRECOMP*, void*, const void*, const void*); -typedef void (*FFTVEC_ADDMUL_FUNCTION)(const CPLX_FFTVEC_ADDMUL_PRECOMP*, void*, const void*, const void*); - -typedef void (*FFTVEC_TWIDDLE_FUNCTION)(const CPLX_FFTVEC_TWIDDLE_PRECOMP*, void*, const void*, const void*); -typedef void (*FFTVEC_BITWIDDLE_FUNCTION)(const CPLX_FFTVEC_BITWIDDLE_PRECOMP*, void*, uint64_t, const void*); - -struct cplx_ifft_precomp { - IFFT_FUNCTION function; - int64_t m; - uint64_t buf_size; - double* powomegas; - void* aligned_buffers; -}; - -struct cplx_fft_precomp { - FFT_FUNCTION function; - int64_t m; - uint64_t buf_size; - double* powomegas; - void* aligned_buffers; -}; - -struct cplx_from_znx32_precomp { - FROM_ZNX32_FUNCTION function; - int64_t m; -}; - -struct cplx_to_znx32_precomp { - TO_ZNX32_FUNCTION function; - int64_t m; - double divisor; -}; - -struct cplx_from_tnx32_precomp { - FROM_TNX32_FUNCTION function; - int64_t m; -}; - -struct cplx_to_tnx32_precomp { - TO_TNX32_FUNCTION function; - int64_t m; - double divisor; -}; - -struct cplx_from_rnx64_precomp { - FROM_RNX64_FUNCTION function; - int64_t m; -}; - -struct cplx_to_rnx64_precomp { - TO_RNX64_FUNCTION function; - int64_t m; - double divisor; -}; - -struct cplx_round_to_rnx64_precomp { - ROUND_TO_RNX64_FUNCTION function; - int64_t m; - double divisor; - uint32_t log2bound; -}; - -typedef struct cplx_mul_precomp { - FFTVEC_MUL_FUNCTION function; - int64_t m; -} CPLX_FFTVEC_MUL_PRECOMP; - -typedef struct cplx_addmul_precomp { - FFTVEC_ADDMUL_FUNCTION function; - int64_t m; -} CPLX_FFTVEC_ADDMUL_PRECOMP; - -struct cplx_twiddle_precomp { - FFTVEC_TWIDDLE_FUNCTION function; - int64_t m; -}; - -struct cplx_bitwiddle_precomp { - FFTVEC_BITWIDDLE_FUNCTION function; - int64_t m; -}; - -EXPORT void cplx_fftvec_twiddle_fma(const CPLX_FFTVEC_TWIDDLE_PRECOMP* tables, void* a, void* b, const void* om); -EXPORT void cplx_fftvec_twiddle_avx512(const CPLX_FFTVEC_TWIDDLE_PRECOMP* tables, void* a, void* b, const void* om); -EXPORT void cplx_fftvec_bitwiddle_fma(const CPLX_FFTVEC_BITWIDDLE_PRECOMP* tables, void* a, uint64_t slice, - const void* om); -EXPORT void cplx_fftvec_bitwiddle_avx512(const CPLX_FFTVEC_BITWIDDLE_PRECOMP* tables, void* a, uint64_t slice, - const void* om); - -#endif // SPQLIOS_CPLX_FFT_PRIVATE_H
@@ -1,367 +0,0 @@
-#include <memory.h> -#include <stdio.h> - -#include "../commons_private.h" -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -/** @brief (a,b) <- (a+omega.b,a-omega.b) */ -void ctwiddle(CPLX a, CPLX b, const CPLX om) { - double re = om[0] * b[0] - om[1] * b[1]; - double im = om[0] * b[1] + om[1] * b[0]; - b[0] = a[0] - re; - b[1] = a[1] - im; - a[0] += re; - a[1] += im; -} - -/** @brief (a,b) <- (a+i.omega.b,a-i.omega.b) */ -void citwiddle(CPLX a, CPLX b, const CPLX om) { - double re = -om[1] * b[0] - om[0] * b[1]; - double im = -om[1] * b[1] + om[0] * b[0]; - b[0] = a[0] - re; - b[1] = a[1] - im; - a[0] += re; - a[1] += im; -} - -/** - * @brief FFT modulo X^16-omega^2 (in registers) - * @param data contains 16 complexes - * @param omega 8 complexes in this order: - * omega,alpha,beta,j.beta,gamma,j.gamma,k.gamma,kj.gamma - * alpha = sqrt(omega), beta = sqrt(alpha), gamma = sqrt(beta) - * j = sqrt(i), k=sqrt(j) - */ -void cplx_fft16_ref(void* data, const void* omega) { - CPLX* d = data; - const CPLX* om = omega; - // first pass - for (uint64_t i = 0; i < 8; ++i) { - ctwiddle(d[0 + i], d[8 + i], om[0]); - } - // - ctwiddle(d[0], d[4], om[1]); - ctwiddle(d[1], d[5], om[1]); - ctwiddle(d[2], d[6], om[1]); - ctwiddle(d[3], d[7], om[1]); - citwiddle(d[8], d[12], om[1]); - citwiddle(d[9], d[13], om[1]); - citwiddle(d[10], d[14], om[1]); - citwiddle(d[11], d[15], om[1]); - // - ctwiddle(d[0], d[2], om[2]); - ctwiddle(d[1], d[3], om[2]); - citwiddle(d[4], d[6], om[2]); - citwiddle(d[5], d[7], om[2]); - ctwiddle(d[8], d[10], om[3]); - ctwiddle(d[9], d[11], om[3]); - citwiddle(d[12], d[14], om[3]); - citwiddle(d[13], d[15], om[3]); - // - ctwiddle(d[0], d[1], om[4]); - citwiddle(d[2], d[3], om[4]); - ctwiddle(d[4], d[5], om[5]); - citwiddle(d[6], d[7], om[5]); - ctwiddle(d[8], d[9], om[6]); - citwiddle(d[10], d[11], om[6]); - ctwiddle(d[12], d[13], om[7]); - citwiddle(d[14], d[15], om[7]); -} - -double cos_2pix(double x) { return m_accurate_cos(2 * M_PI * x); } -double sin_2pix(double x) { return m_accurate_sin(2 * M_PI * x); } -void cplx_set_e2pix(CPLX res, double x) { - res[0] = cos_2pix(x); - res[1] = sin_2pix(x); -} - -void cplx_fft16_precomp(const double entry_pwr, CPLX** omg) { - static const double j_pow = 1. / 8.; - static const double k_pow = 1. / 16.; - const double pom = entry_pwr / 2.; - const double pom_2 = entry_pwr / 4.; - const double pom_4 = entry_pwr / 8.; - const double pom_8 = entry_pwr / 16.; - cplx_set_e2pix((*omg)[0], pom); - cplx_set_e2pix((*omg)[1], pom_2); - cplx_set_e2pix((*omg)[2], pom_4); - cplx_set_e2pix((*omg)[3], pom_4 + j_pow); - cplx_set_e2pix((*omg)[4], pom_8); - cplx_set_e2pix((*omg)[5], pom_8 + j_pow); - cplx_set_e2pix((*omg)[6], pom_8 + k_pow); - cplx_set_e2pix((*omg)[7], pom_8 + j_pow + k_pow); - *omg += 8; -} - -/** - * @brief h twiddles-fft on the same omega - * (also called merge-fft)merges 2 times h evaluations of even/odd polynomials into 2h evaluations of a sigle polynomial - * Input: P_0(z),...,P_{h-1}(z),P_h(z),...,P_{2h-1}(z) - * Output: Q_0(y),...,Q_{h-1}(y),Q_0(-y),...,Q_{h-1}(-y) - * where Q_i(X)=P_i(X^2)+X.P_{h+i}(X^2) and y^2 = z - * @param h number of "coefficients" h >= 1 - * @param data 2h complex coefficients interleaved and 256b aligned - * @param powom y represented as (yre,yim) - */ -void cplx_twiddle_fft_ref(int32_t h, CPLX* data, const CPLX powom) { - CPLX* d0 = data; - CPLX* d1 = data + h; - for (uint64_t i = 0; i < h; ++i) { - ctwiddle(d0[i], d1[i], powom); - } -} - -void cplx_bitwiddle_fft_ref(int32_t h, CPLX* data, const CPLX powom[2]) { - CPLX* d0 = data; - CPLX* d1 = data + h; - CPLX* d2 = data + 2 * h; - CPLX* d3 = data + 3 * h; - for (uint64_t i = 0; i < h; ++i) { - ctwiddle(d0[i], d2[i], powom[0]); - ctwiddle(d1[i], d3[i], powom[0]); - } - for (uint64_t i = 0; i < h; ++i) { - ctwiddle(d0[i], d1[i], powom[1]); - citwiddle(d2[i], d3[i], powom[1]); - } -} - -/** - * Input: P_0(z),P_1(z) - * Output: Q(y),Q(-y) - * where Q(X)=P_0(X^2)+X.P_1(X^2) and y^2 = z - * @param data 2 complexes coefficients interleaved and 256b aligned - * @param powom (z,-z) interleaved: (zre,zim,-zre,-zim) - */ -void merge_fft_last_ref(CPLX* data, const CPLX powom) { - CPLX prod; - cplx_mul(prod, data[1], powom); - cplx_sub(data[1], data[0], prod); - cplx_add(data[0], data[0], prod); -} - -void cplx_fft_ref_bfs_2(CPLX* dat, const CPLX** omg, uint32_t m) { - CPLX* data = (CPLX*)dat; - CPLX* const dend = data + m; - for (int32_t h = m / 2; h >= 2; h >>= 1) { - for (CPLX* d = data; d < dend; d += 2 * h) { - if (memcmp((*omg)[0], (*omg)[1], 8) != 0) abort(); - cplx_twiddle_fft_ref(h, d, **omg); - *omg += 2; - } -#if 0 - printf("after merge %d: ", h); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif - } - for (CPLX* d = data; d < dend; d += 2) { - // TODO see if encoding changes - if ((*omg)[0][0] != -(*omg)[1][0]) abort(); - if ((*omg)[0][1] != -(*omg)[1][1]) abort(); - merge_fft_last_ref(d, **omg); - *omg += 2; - } -#if 0 - printf("after last: "); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif -} - -void cplx_fft_ref_bfs_16(CPLX* dat, const CPLX** omg, uint32_t m) { - CPLX* data = (CPLX*)dat; - CPLX* const dend = data + m; - uint32_t mm = m; - uint32_t log2m = log2(m); - if (log2m % 2 == 1) { - cplx_twiddle_fft_ref(mm / 2, data, **omg); - *omg += 2; - mm >>= 1; - } - while (mm > 16) { - uint32_t h = mm / 4; - for (CPLX* d = data; d < dend; d += mm) { - cplx_bitwiddle_fft_ref(h, d, *omg); - *omg += 2; - } - mm = h; - } - for (CPLX* d = data; d < dend; d += 16) { - cplx_fft16_ref(d, *omg); - *omg += 8; - } -#if 0 - printf("after last: "); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif -} - -/** @brief fft modulo X^m-exp(i.2pi.entry+pwr) -- reference code */ -void cplx_fft_naive(const uint32_t m, const double entry_pwr, CPLX* data) { - if (m == 1) return; - const double pom = entry_pwr / 2.; - const uint32_t h = m / 2; - // apply the twiddle factors - CPLX cpom; - cplx_set_e2pix(cpom, pom); - for (uint64_t i = 0; i < h; ++i) { - ctwiddle(data[i], data[i + h], cpom); - } - // do the recursive calls - cplx_fft_naive(h, pom, data); - cplx_fft_naive(h, pom + 0.5, data + h); -} - -/** @brief fills omega for cplx_fft_bfs_16 modulo X^m-exp(i.2.pi.entry_pwr) */ -void fill_cplx_fft_omegas_bfs_16(const double entry_pwr, CPLX** omg, uint32_t m) { - uint32_t mm = m; - uint32_t log2m = log2(m); - double ss = entry_pwr; - if (log2m % 2 == 1) { - uint32_t h = mm / 2; - double pom = ss / 2.; - for (uint32_t i = 0; i < m / mm; i++) { - cplx_set_e2pix(omg[0][0], pom + fracrevbits(i) / 2.); - cplx_set(omg[0][1], omg[0][0]); - *omg += 2; - } - mm = h; - ss = pom; - } - while (mm > 16) { - double pom = ss / 4.; - uint32_t h = mm / 4; - for (uint32_t i = 0; i < m / mm; i++) { - double om = pom + fracrevbits(i) / 4.; - cplx_set_e2pix(omg[0][0], 2. * om); - cplx_set_e2pix(omg[0][1], om); - *omg += 2; - } - mm = h; - ss = pom; - } - { - // mm=16 - for (uint32_t i = 0; i < m / 16; i++) { - cplx_fft16_precomp(ss + fracrevbits(i), omg); - } - } -} - -/** @brief fills omega for cplx_fft_bfs_2 modulo X^m-exp(i.2.pi.entry_pwr) */ -void fill_cplx_fft_omegas_bfs_2(const double entry_pwr, CPLX** omg, uint32_t m) { - double pom = entry_pwr / 2.; - for (int32_t h = m / 2; h >= 2; h >>= 1) { - for (uint32_t i = 0; i < m / (2 * h); i++) { - cplx_set_e2pix(omg[0][0], pom + fracrevbits(i) / 2.); - cplx_set(omg[0][1], omg[0][0]); - *omg += 2; - } - pom /= 2; - } - { - // h=1 - for (uint32_t i = 0; i < m / 2; i++) { - cplx_set_e2pix((*omg)[0], pom + fracrevbits(i) / 2.); - cplx_neg((*omg)[1], (*omg)[0]); - *omg += 2; - } - } -} - -/** @brief fills omega for cplx_fft_rec modulo X^m-exp(i.2.pi.entry_pwr) */ -void fill_cplx_fft_omegas_rec_16(const double entry_pwr, CPLX** omg, uint32_t m) { - // note that the cases below are for recursive calls only! - // externally, this function shall only be called with m>=4096 - if (m == 1) return; - if (m <= 8) return fill_cplx_fft_omegas_bfs_2(entry_pwr, omg, m); - if (m <= 2048) return fill_cplx_fft_omegas_bfs_16(entry_pwr, omg, m); - double pom = entry_pwr / 2.; - cplx_set_e2pix((*omg)[0], pom); - cplx_set_e2pix((*omg)[1], pom); - *omg += 2; - fill_cplx_fft_omegas_rec_16(pom, omg, m / 2); - fill_cplx_fft_omegas_rec_16(pom + 0.5, omg, m / 2); -} - -void cplx_fft_ref_rec_16(CPLX* dat, const CPLX** omg, uint32_t m) { - if (m == 1) return; - if (m <= 8) return cplx_fft_ref_bfs_2(dat, omg, m); - if (m <= 2048) return cplx_fft_ref_bfs_16(dat, omg, m); - const uint32_t h = m / 2; - if (memcmp((*omg)[0], (*omg)[1], 8) != 0) abort(); - cplx_twiddle_fft_ref(h, dat, **omg); - *omg += 2; - cplx_fft_ref_rec_16(dat, omg, h); - cplx_fft_ref_rec_16(dat + h, omg, h); -} - -void cplx_fft_ref(const CPLX_FFT_PRECOMP* precomp, void* d) { - CPLX* data = (CPLX*)d; - const int32_t m = precomp->m; - const CPLX* omg = (CPLX*)precomp->powomegas; - if (m == 1) return; - if (m <= 8) return cplx_fft_ref_bfs_2(data, &omg, m); - if (m <= 2048) return cplx_fft_ref_bfs_16(data, &omg, m); - cplx_fft_ref_rec_16(data, &omg, m); -} - -EXPORT CPLX_FFT_PRECOMP* new_cplx_fft_precomp(uint32_t m, uint32_t num_buffers) { - const uint64_t OMG_SPACE = ceilto64b((2 * m) * sizeof(CPLX)); - const uint64_t BUF_SIZE = ceilto64b(m * sizeof(CPLX)); - void* reps = malloc(sizeof(CPLX_FFT_PRECOMP) + 63 // padding - + OMG_SPACE // tables //TODO 16? - + num_buffers * BUF_SIZE // buffers - ); - uint64_t aligned_addr = ceilto64b((uint64_t)(reps) + sizeof(CPLX_FFT_PRECOMP)); - CPLX_FFT_PRECOMP* r = (CPLX_FFT_PRECOMP*)reps; - r->m = m; - r->buf_size = BUF_SIZE; - r->powomegas = (double*)aligned_addr; - r->aligned_buffers = (void*)(aligned_addr + OMG_SPACE); - // fill in powomegas - CPLX* omg = (CPLX*)r->powomegas; - if (m <= 8) { - fill_cplx_fft_omegas_bfs_2(0.25, &omg, m); - } else if (m <= 2048) { - fill_cplx_fft_omegas_bfs_16(0.25, &omg, m); - } else { - fill_cplx_fft_omegas_rec_16(0.25, &omg, m); - } - if (((uint64_t)omg) - aligned_addr > OMG_SPACE) abort(); - // dispatch the right implementation - { - if (m <= 4) { - // currently, we do not have any acceletated - // implementation for m<=4 - r->function = cplx_fft_ref; - } else if (CPU_SUPPORTS("fma")) { - r->function = cplx_fft_avx2_fma; - } else { - r->function = cplx_fft_ref; - } - } - return reps; -} - -EXPORT void* cplx_fft_precomp_get_buffer(const CPLX_FFT_PRECOMP* tables, uint32_t buffer_index) { - return (uint8_t*)tables->aligned_buffers + buffer_index * tables->buf_size; -} - -EXPORT void cplx_fft_simple(uint32_t m, void* data) { - static CPLX_FFT_PRECOMP* p[31] = {0}; - CPLX_FFT_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_cplx_fft_precomp(m, 0); - (*f)->function(*f, data); -} - -EXPORT void cplx_fft(const CPLX_FFT_PRECOMP* tables, void* data) { tables->function(tables, data); }
@@ -1,309 +0,0 @@
-#include <immintrin.h> - -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -typedef double D2MEM[2]; - -EXPORT void cplx_fftvec_addmul_sse(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const D2MEM* aa = (D2MEM*)a; - const D2MEM* bb = (D2MEM*)b; - D2MEM* rr = (D2MEM*)r; - const D2MEM* const aend = aa + m; - do { - /* -BEGIN_TEMPLATE -const __m128d ari% = _mm_loadu_pd(aa[%]); -const __m128d bri% = _mm_loadu_pd(bb[%]); -const __m128d rri% = _mm_loadu_pd(rr[%]); -const __m128d bir% = _mm_shuffle_pd(bri%,bri%, 5); -const __m128d aii% = _mm_shuffle_pd(ari%,ari%, 15); -const __m128d pro% = _mm_fmaddsub_pd(aii%,bir%,rri%); -const __m128d arr% = _mm_shuffle_pd(ari%,ari%, 0); -const __m128d res% = _mm_fmaddsub_pd(arr%,bri%,pro%); -_mm_storeu_pd(rr[%],res%); -rr += @; // ONCE -aa += @; // ONCE -bb += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 2 - const __m128d ari0 = _mm_loadu_pd(aa[0]); - const __m128d ari1 = _mm_loadu_pd(aa[1]); - const __m128d bri0 = _mm_loadu_pd(bb[0]); - const __m128d bri1 = _mm_loadu_pd(bb[1]); - const __m128d rri0 = _mm_loadu_pd(rr[0]); - const __m128d rri1 = _mm_loadu_pd(rr[1]); - const __m128d bir0 = _mm_shuffle_pd(bri0, bri0, 0b01); - const __m128d bir1 = _mm_shuffle_pd(bri1, bri1, 0b01); - const __m128d aii0 = _mm_shuffle_pd(ari0, ari0, 0b11); - const __m128d aii1 = _mm_shuffle_pd(ari1, ari1, 0b11); - const __m128d pro0 = _mm_fmaddsub_pd(aii0, bir0, rri0); - const __m128d pro1 = _mm_fmaddsub_pd(aii1, bir1, rri1); - const __m128d arr0 = _mm_shuffle_pd(ari0, ari0, 0b00); - const __m128d arr1 = _mm_shuffle_pd(ari1, ari1, 0b00); - const __m128d res0 = _mm_fmaddsub_pd(arr0, bri0, pro0); - const __m128d res1 = _mm_fmaddsub_pd(arr1, bri1, pro1); - _mm_storeu_pd(rr[0], res0); - _mm_storeu_pd(rr[1], res1); - rr += 2; // ONCE - aa += 2; // ONCE - bb += 2; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -#if 0 -EXPORT void cplx_fftvec_mul_fma(const CPLX_FFTVEC_MUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%, 5); // conj of b -const __m256d aii% = _mm256_shuffle_pd(ari%,ari%, 15); // im of a -const __m256d pro% = _mm256_mul_pd(aii%,bir%); -const __m256d arr% = _mm256_shuffle_pd(ari%,ari%, 0); // rr of a -const __m256d res% = _mm256_fmaddsub_pd(arr%,bri%,pro%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d bir0 = _mm256_shuffle_pd(bri0,bri0, 5); // conj of b -const __m256d bir1 = _mm256_shuffle_pd(bri1,bri1, 5); // conj of b -const __m256d bir2 = _mm256_shuffle_pd(bri2,bri2, 5); // conj of b -const __m256d bir3 = _mm256_shuffle_pd(bri3,bri3, 5); // conj of b -const __m256d aii0 = _mm256_shuffle_pd(ari0,ari0, 15); // im of a -const __m256d aii1 = _mm256_shuffle_pd(ari1,ari1, 15); // im of a -const __m256d aii2 = _mm256_shuffle_pd(ari2,ari2, 15); // im of a -const __m256d aii3 = _mm256_shuffle_pd(ari3,ari3, 15); // im of a -const __m256d pro0 = _mm256_mul_pd(aii0,bir0); -const __m256d pro1 = _mm256_mul_pd(aii1,bir1); -const __m256d pro2 = _mm256_mul_pd(aii2,bir2); -const __m256d pro3 = _mm256_mul_pd(aii3,bir3); -const __m256d arr0 = _mm256_shuffle_pd(ari0,ari0, 0); // rr of a -const __m256d arr1 = _mm256_shuffle_pd(ari1,ari1, 0); // rr of a -const __m256d arr2 = _mm256_shuffle_pd(ari2,ari2, 0); // rr of a -const __m256d arr3 = _mm256_shuffle_pd(ari3,ari3, 0); // rr of a -const __m256d res0 = _mm256_fmaddsub_pd(arr0,bri0,pro0); -const __m256d res1 = _mm256_fmaddsub_pd(arr1,bri1,pro1); -const __m256d res2 = _mm256_fmaddsub_pd(arr2,bri2,pro2); -const __m256d res3 = _mm256_fmaddsub_pd(arr3,bri3,pro3); -_mm256_storeu_pd(rr[0],res0); -_mm256_storeu_pd(rr[1],res1); -_mm256_storeu_pd(rr[2],res2); -_mm256_storeu_pd(rr[3],res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_add_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d res% = _mm256_add_pd(ari%,bri%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d res0 = _mm256_add_pd(ari0,bri0); -const __m256d res1 = _mm256_add_pd(ari1,bri1); -const __m256d res2 = _mm256_add_pd(ari2,bri2); -const __m256d res3 = _mm256_add_pd(ari3,bri3); -_mm256_storeu_pd(rr[0],res0); -_mm256_storeu_pd(rr[1],res1); -_mm256_storeu_pd(rr[2],res2); -_mm256_storeu_pd(rr[3],res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_sub2_to_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d sum% = _mm256_add_pd(ari%,bri%); -const __m256d rri% = _mm256_loadu_pd(rr[%]); -const __m256d res% = _mm256_sub_pd(rri%,sum%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d sum0 = _mm256_add_pd(ari0,bri0); -const __m256d sum1 = _mm256_add_pd(ari1,bri1); -const __m256d sum2 = _mm256_add_pd(ari2,bri2); -const __m256d sum3 = _mm256_add_pd(ari3,bri3); -const __m256d rri0 = _mm256_loadu_pd(rr[0]); -const __m256d rri1 = _mm256_loadu_pd(rr[1]); -const __m256d rri2 = _mm256_loadu_pd(rr[2]); -const __m256d rri3 = _mm256_loadu_pd(rr[3]); -const __m256d res0 = _mm256_sub_pd(rri0,sum0); -const __m256d res1 = _mm256_sub_pd(rri1,sum1); -const __m256d res2 = _mm256_sub_pd(rri2,sum2); -const __m256d res3 = _mm256_sub_pd(rri3,sum3); -_mm256_storeu_pd(rr[0],res0); -_mm256_storeu_pd(rr[1],res1); -_mm256_storeu_pd(rr[2],res2); -_mm256_storeu_pd(rr[3],res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_copy_fma(uint32_t m, void* r, const void* a) { - const double(*aa)[4] = (double(*)[4])a; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -_mm256_storeu_pd(rr[%],ari%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -_mm256_storeu_pd(rr[0],ari0); -_mm256_storeu_pd(rr[1],ari1); -_mm256_storeu_pd(rr[2],ari2); -_mm256_storeu_pd(rr[3],ari3); - // END_INTERLEAVE - rr += 4; - aa += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_twiddle_fma(uint32_t m, void* a, void* b, const void* omg) { - double(*aa)[4] = (double(*)[4])a; - double(*bb)[4] = (double(*)[4])b; - const double(*const aend)[4] = aa + (m >> 1); - const __m256d om = _mm256_loadu_pd(omg); - const __m256d omrr = _mm256_shuffle_pd(om, om, 0); - const __m256d omii = _mm256_shuffle_pd(om, om, 15); - do { - /* -BEGIN_TEMPLATE -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%,5); -__m256d p% = _mm256_mul_pd(bir%,omii); -p% = _mm256_fmaddsub_pd(bri%,omrr,p%); -const __m256d ari% = _mm256_loadu_pd(aa[%]); -_mm256_storeu_pd(aa[%],_mm256_add_pd(ari%,p%)); -_mm256_storeu_pd(bb[%],_mm256_sub_pd(ari%,p%)); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 -const __m256d bri0 = _mm256_loadu_pd(bb[0]); -const __m256d bri1 = _mm256_loadu_pd(bb[1]); -const __m256d bri2 = _mm256_loadu_pd(bb[2]); -const __m256d bri3 = _mm256_loadu_pd(bb[3]); -const __m256d bir0 = _mm256_shuffle_pd(bri0,bri0,5); -const __m256d bir1 = _mm256_shuffle_pd(bri1,bri1,5); -const __m256d bir2 = _mm256_shuffle_pd(bri2,bri2,5); -const __m256d bir3 = _mm256_shuffle_pd(bri3,bri3,5); -__m256d p0 = _mm256_mul_pd(bir0,omii); -__m256d p1 = _mm256_mul_pd(bir1,omii); -__m256d p2 = _mm256_mul_pd(bir2,omii); -__m256d p3 = _mm256_mul_pd(bir3,omii); -p0 = _mm256_fmaddsub_pd(bri0,omrr,p0); -p1 = _mm256_fmaddsub_pd(bri1,omrr,p1); -p2 = _mm256_fmaddsub_pd(bri2,omrr,p2); -p3 = _mm256_fmaddsub_pd(bri3,omrr,p3); -const __m256d ari0 = _mm256_loadu_pd(aa[0]); -const __m256d ari1 = _mm256_loadu_pd(aa[1]); -const __m256d ari2 = _mm256_loadu_pd(aa[2]); -const __m256d ari3 = _mm256_loadu_pd(aa[3]); -_mm256_storeu_pd(aa[0],_mm256_add_pd(ari0,p0)); -_mm256_storeu_pd(aa[1],_mm256_add_pd(ari1,p1)); -_mm256_storeu_pd(aa[2],_mm256_add_pd(ari2,p2)); -_mm256_storeu_pd(aa[3],_mm256_add_pd(ari3,p3)); -_mm256_storeu_pd(bb[0],_mm256_sub_pd(ari0,p0)); -_mm256_storeu_pd(bb[1],_mm256_sub_pd(ari1,p1)); -_mm256_storeu_pd(bb[2],_mm256_sub_pd(ari2,p2)); -_mm256_storeu_pd(bb[3],_mm256_sub_pd(ari3,p3)); - // END_INTERLEAVE - bb += 4; - aa += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_innerprod_avx2_fma(const CPLX_FFTVEC_INNERPROD_PRECOMP* precomp, const int32_t ellbar, - const uint64_t lda, const uint64_t ldb, - void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const uint32_t blk = precomp->blk; - const uint32_t nblocks = precomp->nblocks; - const CPLX* aa = (CPLX*)a; - const CPLX* bb = (CPLX*)b; - CPLX* rr = (CPLX*)r; - const uint64_t ldda = lda >> 4; // in CPLX - const uint64_t lddb = ldb >> 4; - if (m==0) { - memset(r, 0, m*sizeof(CPLX)); - return; - } - for (uint32_t k=0; k<nblocks; ++k) { - const uint64_t offset = k*blk; - const CPLX* aaa = aa+offset; - const CPLX* bbb = bb+offset; - CPLX *rrr = rr+offset; - cplx_fftvec_mul_fma(&precomp->mul_func, rrr, aaa, bbb); - for (int32_t i=1; i<ellbar; ++i) { - cplx_fftvec_addmul_fma(&precomp->addmul_func, rrr, aaa + i * ldda, bbb + i * lddb); - } - } -} -#endif
@@ -1,387 +0,0 @@
-#include <immintrin.h> -#include <string.h> - -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -typedef double D4MEM[4]; - -EXPORT void cplx_fftvec_mul_fma(const CPLX_FFTVEC_MUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%, 5); // conj of b -const __m256d aii% = _mm256_shuffle_pd(ari%,ari%, 15); // im of a -const __m256d pro% = _mm256_mul_pd(aii%,bir%); -const __m256d arr% = _mm256_shuffle_pd(ari%,ari%, 0); // rr of a -const __m256d res% = _mm256_fmaddsub_pd(arr%,bri%,pro%); -_mm256_storeu_pd(rr[%],res%); -rr += @; // ONCE -aa += @; // ONCE -bb += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - const __m256d ari0 = _mm256_loadu_pd(aa[0]); - const __m256d ari1 = _mm256_loadu_pd(aa[1]); - const __m256d ari2 = _mm256_loadu_pd(aa[2]); - const __m256d ari3 = _mm256_loadu_pd(aa[3]); - const __m256d bri0 = _mm256_loadu_pd(bb[0]); - const __m256d bri1 = _mm256_loadu_pd(bb[1]); - const __m256d bri2 = _mm256_loadu_pd(bb[2]); - const __m256d bri3 = _mm256_loadu_pd(bb[3]); - const __m256d bir0 = _mm256_shuffle_pd(bri0, bri0, 5); // conj of b - const __m256d bir1 = _mm256_shuffle_pd(bri1, bri1, 5); // conj of b - const __m256d bir2 = _mm256_shuffle_pd(bri2, bri2, 5); // conj of b - const __m256d bir3 = _mm256_shuffle_pd(bri3, bri3, 5); // conj of b - const __m256d aii0 = _mm256_shuffle_pd(ari0, ari0, 15); // im of a - const __m256d aii1 = _mm256_shuffle_pd(ari1, ari1, 15); // im of a - const __m256d aii2 = _mm256_shuffle_pd(ari2, ari2, 15); // im of a - const __m256d aii3 = _mm256_shuffle_pd(ari3, ari3, 15); // im of a - const __m256d pro0 = _mm256_mul_pd(aii0, bir0); - const __m256d pro1 = _mm256_mul_pd(aii1, bir1); - const __m256d pro2 = _mm256_mul_pd(aii2, bir2); - const __m256d pro3 = _mm256_mul_pd(aii3, bir3); - const __m256d arr0 = _mm256_shuffle_pd(ari0, ari0, 0); // rr of a - const __m256d arr1 = _mm256_shuffle_pd(ari1, ari1, 0); // rr of a - const __m256d arr2 = _mm256_shuffle_pd(ari2, ari2, 0); // rr of a - const __m256d arr3 = _mm256_shuffle_pd(ari3, ari3, 0); // rr of a - const __m256d res0 = _mm256_fmaddsub_pd(arr0, bri0, pro0); - const __m256d res1 = _mm256_fmaddsub_pd(arr1, bri1, pro1); - const __m256d res2 = _mm256_fmaddsub_pd(arr2, bri2, pro2); - const __m256d res3 = _mm256_fmaddsub_pd(arr3, bri3, pro3); - _mm256_storeu_pd(rr[0], res0); - _mm256_storeu_pd(rr[1], res1); - _mm256_storeu_pd(rr[2], res2); - _mm256_storeu_pd(rr[3], res3); - rr += 4; // ONCE - aa += 4; // ONCE - bb += 4; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -EXPORT void cplx_fftvec_addmul_fma(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d rri% = _mm256_loadu_pd(rr[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%, 5); -const __m256d aii% = _mm256_shuffle_pd(ari%,ari%, 15); -const __m256d pro% = _mm256_fmaddsub_pd(aii%,bir%,rri%); -const __m256d arr% = _mm256_shuffle_pd(ari%,ari%, 0); -const __m256d res% = _mm256_fmaddsub_pd(arr%,bri%,pro%); -_mm256_storeu_pd(rr[%],res%); -rr += @; // ONCE -aa += @; // ONCE -bb += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 2 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - const __m256d ari0 = _mm256_loadu_pd(aa[0]); - const __m256d ari1 = _mm256_loadu_pd(aa[1]); - const __m256d bri0 = _mm256_loadu_pd(bb[0]); - const __m256d bri1 = _mm256_loadu_pd(bb[1]); - const __m256d rri0 = _mm256_loadu_pd(rr[0]); - const __m256d rri1 = _mm256_loadu_pd(rr[1]); - const __m256d bir0 = _mm256_shuffle_pd(bri0, bri0, 5); - const __m256d bir1 = _mm256_shuffle_pd(bri1, bri1, 5); - const __m256d aii0 = _mm256_shuffle_pd(ari0, ari0, 15); - const __m256d aii1 = _mm256_shuffle_pd(ari1, ari1, 15); - const __m256d pro0 = _mm256_fmaddsub_pd(aii0, bir0, rri0); - const __m256d pro1 = _mm256_fmaddsub_pd(aii1, bir1, rri1); - const __m256d arr0 = _mm256_shuffle_pd(ari0, ari0, 0); - const __m256d arr1 = _mm256_shuffle_pd(ari1, ari1, 0); - const __m256d res0 = _mm256_fmaddsub_pd(arr0, bri0, pro0); - const __m256d res1 = _mm256_fmaddsub_pd(arr1, bri1, pro1); - _mm256_storeu_pd(rr[0], res0); - _mm256_storeu_pd(rr[1], res1); - rr += 2; // ONCE - aa += 2; // ONCE - bb += 2; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -EXPORT void cplx_fftvec_bitwiddle_fma(const CPLX_FFTVEC_BITWIDDLE_PRECOMP* precomp, void* a, uint64_t slicea, - const void* omg) { - const uint32_t m = precomp->m; - const uint64_t OFFSET = slicea / sizeof(D4MEM); - D4MEM* aa = (D4MEM*)a; - const double(*const aend)[4] = aa + (m >> 1); - const __m256d om = _mm256_loadu_pd(omg); - const __m256d om1rr = _mm256_shuffle_pd(om, om, 0); - const __m256d om1ii = _mm256_shuffle_pd(om, om, 15); - const __m256d om2rr = _mm256_shuffle_pd(om, om, 0); - const __m256d om2ii = _mm256_shuffle_pd(om, om, 0); - const __m256d om3rr = _mm256_shuffle_pd(om, om, 15); - const __m256d om3ii = _mm256_shuffle_pd(om, om, 15); - do { - /* -BEGIN_TEMPLATE -__m256d ari% = _mm256_loadu_pd(aa[%]); -__m256d bri% = _mm256_loadu_pd((aa+OFFSET)[%]); -__m256d cri% = _mm256_loadu_pd((aa+2*OFFSET)[%]); -__m256d dri% = _mm256_loadu_pd((aa+3*OFFSET)[%]); -__m256d pa% = _mm256_shuffle_pd(cri%,cri%,5); -__m256d pb% = _mm256_shuffle_pd(dri%,dri%,5); -pa% = _mm256_mul_pd(pa%,om1ii); -pb% = _mm256_mul_pd(pb%,om1ii); -pa% = _mm256_fmaddsub_pd(cri%,om1rr,pa%); -pb% = _mm256_fmaddsub_pd(dri%,om1rr,pb%); -cri% = _mm256_sub_pd(ari%,pa%); -dri% = _mm256_sub_pd(bri%,pb%); -ari% = _mm256_add_pd(ari%,pa%); -bri% = _mm256_add_pd(bri%,pb%); -pa% = _mm256_shuffle_pd(bri%,bri%,5); -pb% = _mm256_shuffle_pd(dri%,dri%,5); -pa% = _mm256_mul_pd(pa%,om2ii); -pb% = _mm256_mul_pd(pb%,om3ii); -pa% = _mm256_fmaddsub_pd(bri%,om2rr,pa%); -pb% = _mm256_fmaddsub_pd(dri%,om3rr,pb%); -bri% = _mm256_sub_pd(ari%,pa%); -dri% = _mm256_sub_pd(cri%,pb%); -ari% = _mm256_add_pd(ari%,pa%); -cri% = _mm256_add_pd(cri%,pb%); -_mm256_storeu_pd(aa[%], ari%); -_mm256_storeu_pd((aa+OFFSET)[%],bri%); -_mm256_storeu_pd((aa+2*OFFSET)[%],cri%); -_mm256_storeu_pd((aa+3*OFFSET)[%],dri%); -aa += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 1 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - __m256d ari0 = _mm256_loadu_pd(aa[0]); - __m256d bri0 = _mm256_loadu_pd((aa + OFFSET)[0]); - __m256d cri0 = _mm256_loadu_pd((aa + 2 * OFFSET)[0]); - __m256d dri0 = _mm256_loadu_pd((aa + 3 * OFFSET)[0]); - __m256d pa0 = _mm256_shuffle_pd(cri0, cri0, 5); - __m256d pb0 = _mm256_shuffle_pd(dri0, dri0, 5); - pa0 = _mm256_mul_pd(pa0, om1ii); - pb0 = _mm256_mul_pd(pb0, om1ii); - pa0 = _mm256_fmaddsub_pd(cri0, om1rr, pa0); - pb0 = _mm256_fmaddsub_pd(dri0, om1rr, pb0); - cri0 = _mm256_sub_pd(ari0, pa0); - dri0 = _mm256_sub_pd(bri0, pb0); - ari0 = _mm256_add_pd(ari0, pa0); - bri0 = _mm256_add_pd(bri0, pb0); - pa0 = _mm256_shuffle_pd(bri0, bri0, 5); - pb0 = _mm256_shuffle_pd(dri0, dri0, 5); - pa0 = _mm256_mul_pd(pa0, om2ii); - pb0 = _mm256_mul_pd(pb0, om3ii); - pa0 = _mm256_fmaddsub_pd(bri0, om2rr, pa0); - pb0 = _mm256_fmaddsub_pd(dri0, om3rr, pb0); - bri0 = _mm256_sub_pd(ari0, pa0); - dri0 = _mm256_sub_pd(cri0, pb0); - ari0 = _mm256_add_pd(ari0, pa0); - cri0 = _mm256_add_pd(cri0, pb0); - _mm256_storeu_pd(aa[0], ari0); - _mm256_storeu_pd((aa + OFFSET)[0], bri0); - _mm256_storeu_pd((aa + 2 * OFFSET)[0], cri0); - _mm256_storeu_pd((aa + 3 * OFFSET)[0], dri0); - aa += 1; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -EXPORT void cplx_fftvec_sub2_to_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d sum% = _mm256_add_pd(ari%,bri%); -const __m256d rri% = _mm256_loadu_pd(rr[%]); -const __m256d res% = _mm256_sub_pd(rri%,sum%); -_mm256_storeu_pd(rr[%],res%); -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - const __m256d ari0 = _mm256_loadu_pd(aa[0]); - const __m256d ari1 = _mm256_loadu_pd(aa[1]); - const __m256d ari2 = _mm256_loadu_pd(aa[2]); - const __m256d ari3 = _mm256_loadu_pd(aa[3]); - const __m256d bri0 = _mm256_loadu_pd(bb[0]); - const __m256d bri1 = _mm256_loadu_pd(bb[1]); - const __m256d bri2 = _mm256_loadu_pd(bb[2]); - const __m256d bri3 = _mm256_loadu_pd(bb[3]); - const __m256d sum0 = _mm256_add_pd(ari0, bri0); - const __m256d sum1 = _mm256_add_pd(ari1, bri1); - const __m256d sum2 = _mm256_add_pd(ari2, bri2); - const __m256d sum3 = _mm256_add_pd(ari3, bri3); - const __m256d rri0 = _mm256_loadu_pd(rr[0]); - const __m256d rri1 = _mm256_loadu_pd(rr[1]); - const __m256d rri2 = _mm256_loadu_pd(rr[2]); - const __m256d rri3 = _mm256_loadu_pd(rr[3]); - const __m256d res0 = _mm256_sub_pd(rri0, sum0); - const __m256d res1 = _mm256_sub_pd(rri1, sum1); - const __m256d res2 = _mm256_sub_pd(rri2, sum2); - const __m256d res3 = _mm256_sub_pd(rri3, sum3); - _mm256_storeu_pd(rr[0], res0); - _mm256_storeu_pd(rr[1], res1); - _mm256_storeu_pd(rr[2], res2); - _mm256_storeu_pd(rr[3], res3); - // END_INTERLEAVE - rr += 4; - aa += 4; - bb += 4; - } while (aa < aend); -} - -EXPORT void cplx_fftvec_add_fma(uint32_t m, void* r, const void* a, const void* b) { - const double(*aa)[4] = (double(*)[4])a; - const double(*bb)[4] = (double(*)[4])b; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* - BEGIN_TEMPLATE - const __m256d ari% = _mm256_loadu_pd(aa[%]); - const __m256d bri% = _mm256_loadu_pd(bb[%]); - const __m256d res% = _mm256_add_pd(ari%,bri%); - _mm256_storeu_pd(rr[%],res%); - rr += @; // ONCE - aa += @; // ONCE - bb += @; // ONCE - END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - const __m256d ari0 = _mm256_loadu_pd(aa[0]); - const __m256d ari1 = _mm256_loadu_pd(aa[1]); - const __m256d ari2 = _mm256_loadu_pd(aa[2]); - const __m256d ari3 = _mm256_loadu_pd(aa[3]); - const __m256d bri0 = _mm256_loadu_pd(bb[0]); - const __m256d bri1 = _mm256_loadu_pd(bb[1]); - const __m256d bri2 = _mm256_loadu_pd(bb[2]); - const __m256d bri3 = _mm256_loadu_pd(bb[3]); - const __m256d res0 = _mm256_add_pd(ari0, bri0); - const __m256d res1 = _mm256_add_pd(ari1, bri1); - const __m256d res2 = _mm256_add_pd(ari2, bri2); - const __m256d res3 = _mm256_add_pd(ari3, bri3); - _mm256_storeu_pd(rr[0], res0); - _mm256_storeu_pd(rr[1], res1); - _mm256_storeu_pd(rr[2], res2); - _mm256_storeu_pd(rr[3], res3); - rr += 4; // ONCE - aa += 4; // ONCE - bb += 4; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -EXPORT void cplx_fftvec_twiddle_fma(const CPLX_FFTVEC_TWIDDLE_PRECOMP* precomp, void* a, void* b, const void* omg) { - const uint32_t m = precomp->m; - double(*aa)[4] = (double(*)[4])a; - double(*bb)[4] = (double(*)[4])b; - const double(*const aend)[4] = aa + (m >> 1); - const __m256d om = _mm256_loadu_pd(omg); - const __m256d omrr = _mm256_shuffle_pd(om, om, 0); - const __m256d omii = _mm256_shuffle_pd(om, om, 15); - do { - /* -BEGIN_TEMPLATE -const __m256d bri% = _mm256_loadu_pd(bb[%]); -const __m256d bir% = _mm256_shuffle_pd(bri%,bri%,5); -__m256d p% = _mm256_mul_pd(bir%,omii); -p% = _mm256_fmaddsub_pd(bri%,omrr,p%); -const __m256d ari% = _mm256_loadu_pd(aa[%]); -_mm256_storeu_pd(aa[%],_mm256_add_pd(ari%,p%)); -_mm256_storeu_pd(bb[%],_mm256_sub_pd(ari%,p%)); -bb += @; // ONCE -aa += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - const __m256d bri0 = _mm256_loadu_pd(bb[0]); - const __m256d bri1 = _mm256_loadu_pd(bb[1]); - const __m256d bri2 = _mm256_loadu_pd(bb[2]); - const __m256d bri3 = _mm256_loadu_pd(bb[3]); - const __m256d bir0 = _mm256_shuffle_pd(bri0, bri0, 5); - const __m256d bir1 = _mm256_shuffle_pd(bri1, bri1, 5); - const __m256d bir2 = _mm256_shuffle_pd(bri2, bri2, 5); - const __m256d bir3 = _mm256_shuffle_pd(bri3, bri3, 5); - __m256d p0 = _mm256_mul_pd(bir0, omii); - __m256d p1 = _mm256_mul_pd(bir1, omii); - __m256d p2 = _mm256_mul_pd(bir2, omii); - __m256d p3 = _mm256_mul_pd(bir3, omii); - p0 = _mm256_fmaddsub_pd(bri0, omrr, p0); - p1 = _mm256_fmaddsub_pd(bri1, omrr, p1); - p2 = _mm256_fmaddsub_pd(bri2, omrr, p2); - p3 = _mm256_fmaddsub_pd(bri3, omrr, p3); - const __m256d ari0 = _mm256_loadu_pd(aa[0]); - const __m256d ari1 = _mm256_loadu_pd(aa[1]); - const __m256d ari2 = _mm256_loadu_pd(aa[2]); - const __m256d ari3 = _mm256_loadu_pd(aa[3]); - _mm256_storeu_pd(aa[0], _mm256_add_pd(ari0, p0)); - _mm256_storeu_pd(aa[1], _mm256_add_pd(ari1, p1)); - _mm256_storeu_pd(aa[2], _mm256_add_pd(ari2, p2)); - _mm256_storeu_pd(aa[3], _mm256_add_pd(ari3, p3)); - _mm256_storeu_pd(bb[0], _mm256_sub_pd(ari0, p0)); - _mm256_storeu_pd(bb[1], _mm256_sub_pd(ari1, p1)); - _mm256_storeu_pd(bb[2], _mm256_sub_pd(ari2, p2)); - _mm256_storeu_pd(bb[3], _mm256_sub_pd(ari3, p3)); - bb += 4; // ONCE - aa += 4; // ONCE - // END_INTERLEAVE - } while (aa < aend); -} - -EXPORT void cplx_fftvec_copy_fma(uint32_t m, void* r, const void* a) { - const double(*aa)[4] = (double(*)[4])a; - double(*rr)[4] = (double(*)[4])r; - const double(*const aend)[4] = aa + (m >> 1); - do { - /* -BEGIN_TEMPLATE -const __m256d ari% = _mm256_loadu_pd(aa[%]); -_mm256_storeu_pd(rr[%],ari%); -rr += @; // ONCE -aa += @; // ONCE -END_TEMPLATE - */ - // BEGIN_INTERLEAVE 4 - // This block is automatically generated from the template above - // by the interleave.pl script. Please do not edit by hand - const __m256d ari0 = _mm256_loadu_pd(aa[0]); - const __m256d ari1 = _mm256_loadu_pd(aa[1]); - const __m256d ari2 = _mm256_loadu_pd(aa[2]); - const __m256d ari3 = _mm256_loadu_pd(aa[3]); - _mm256_storeu_pd(rr[0], ari0); - _mm256_storeu_pd(rr[1], ari1); - _mm256_storeu_pd(rr[2], ari2); - _mm256_storeu_pd(rr[3], ari3); - rr += 4; // ONCE - aa += 4; // ONCE - // END_INTERLEAVE - } while (aa < aend); -}
@@ -1,85 +0,0 @@
-#include <string.h> - -#include "../commons_private.h" -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -EXPORT void cplx_fftvec_addmul_ref(const CPLX_FFTVEC_ADDMUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const CPLX* aa = (CPLX*)a; - const CPLX* bb = (CPLX*)b; - CPLX* rr = (CPLX*)r; - for (uint32_t i = 0; i < m; ++i) { - const double re = aa[i][0] * bb[i][0] - aa[i][1] * bb[i][1]; - const double im = aa[i][0] * bb[i][1] + aa[i][1] * bb[i][0]; - rr[i][0] += re; - rr[i][1] += im; - } -} - -EXPORT void cplx_fftvec_mul_ref(const CPLX_FFTVEC_MUL_PRECOMP* precomp, void* r, const void* a, const void* b) { - const uint32_t m = precomp->m; - const CPLX* aa = (CPLX*)a; - const CPLX* bb = (CPLX*)b; - CPLX* rr = (CPLX*)r; - for (uint32_t i = 0; i < m; ++i) { - const double re = aa[i][0] * bb[i][0] - aa[i][1] * bb[i][1]; - const double im = aa[i][0] * bb[i][1] + aa[i][1] * bb[i][0]; - rr[i][0] = re; - rr[i][1] = im; - } -} - -EXPORT void* init_cplx_fftvec_addmul_precomp(CPLX_FFTVEC_ADDMUL_PRECOMP* r, uint32_t m) { - if (m & (m - 1)) return spqlios_error("m must be a power of two"); - r->m = m; - if (m <= 4) { - r->function = cplx_fftvec_addmul_ref; - } else if (CPU_SUPPORTS("fma")) { - r->function = cplx_fftvec_addmul_fma; - } else { - r->function = cplx_fftvec_addmul_ref; - } - return r; -} - -EXPORT void* init_cplx_fftvec_mul_precomp(CPLX_FFTVEC_MUL_PRECOMP* r, uint32_t m) { - if (m & (m - 1)) return spqlios_error("m must be a power of two"); - r->m = m; - if (m <= 4) { - r->function = cplx_fftvec_mul_ref; - } else if (CPU_SUPPORTS("fma")) { - r->function = cplx_fftvec_mul_fma; - } else { - r->function = cplx_fftvec_mul_ref; - } - return r; -} - -EXPORT CPLX_FFTVEC_ADDMUL_PRECOMP* new_cplx_fftvec_addmul_precomp(uint32_t m) { - CPLX_FFTVEC_ADDMUL_PRECOMP* r = malloc(sizeof(CPLX_FFTVEC_MUL_PRECOMP)); - return spqlios_keep_or_free(r, init_cplx_fftvec_addmul_precomp(r, m)); -} - -EXPORT CPLX_FFTVEC_MUL_PRECOMP* new_cplx_fftvec_mul_precomp(uint32_t m) { - CPLX_FFTVEC_MUL_PRECOMP* r = malloc(sizeof(CPLX_FFTVEC_MUL_PRECOMP)); - return spqlios_keep_or_free(r, init_cplx_fftvec_mul_precomp(r, m)); -} - -EXPORT void cplx_fftvec_mul_simple(uint32_t m, void* r, const void* a, const void* b) { - static CPLX_FFTVEC_MUL_PRECOMP p[31] = {0}; - CPLX_FFTVEC_MUL_PRECOMP* f = p + log2m(m); - if (!f->function) { - if (!init_cplx_fftvec_mul_precomp(f, m)) abort(); - } - f->function(f, r, a, b); -} - -EXPORT void cplx_fftvec_addmul_simple(uint32_t m, void* r, const void* a, const void* b) { - static CPLX_FFTVEC_ADDMUL_PRECOMP p[31] = {0}; - CPLX_FFTVEC_ADDMUL_PRECOMP* f = p + log2m(m); - if (!f->function) { - if (!init_cplx_fftvec_addmul_precomp(f, m)) abort(); - } - f->function(f, r, a, b); -}
@@ -1,157 +0,0 @@
-# shifted FFT over X^16-i -# 1st argument (rdi) contains 16 complexes -# 2nd argument (rsi) contains: 8 complexes -# omega,alpha,beta,j.beta,gamma,j.gamma,k.gamma,kj.gamma -# alpha = sqrt(omega), beta = sqrt(alpha), gamma = sqrt(beta) -# j = sqrt(i), k=sqrt(j) -.globl cplx_ifft16_avx_fma -cplx_ifft16_avx_fma: -vmovupd (%rdi),%ymm8 # load data into registers %ymm8 -> %ymm15 -vmovupd 0x20(%rdi),%ymm9 -vmovupd 0x40(%rdi),%ymm10 -vmovupd 0x60(%rdi),%ymm11 -vmovupd 0x80(%rdi),%ymm12 -vmovupd 0xa0(%rdi),%ymm13 -vmovupd 0xc0(%rdi),%ymm14 -vmovupd 0xe0(%rdi),%ymm15 - -.fourth_pass: -vmovupd 0(%rsi),%ymm0 /* gamma */ -vmovupd 32(%rsi),%ymm2 /* delta */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vperm2f128 $0x31,%ymm10,%ymm8,%ymm4 # ymm4 contains c1,c5 -vperm2f128 $0x31,%ymm11,%ymm9,%ymm5 # ymm5 contains c3,c7 -vperm2f128 $0x31,%ymm14,%ymm12,%ymm6 # ymm6 contains c9,c13 -vperm2f128 $0x31,%ymm15,%ymm13,%ymm7 # ymm7 contains c11,c15 -vperm2f128 $0x20,%ymm10,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm11,%ymm9,%ymm9 # ymm9 contains c2,c6 -vperm2f128 $0x20,%ymm14,%ymm12,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x20,%ymm15,%ymm13,%ymm11 # ymm11 contains c10,c14 -vsubpd %ymm4,%ymm8,%ymm12 # tw: to mul by gamma -vsubpd %ymm5,%ymm9,%ymm13 # itw: to mul by i.gamma -vsubpd %ymm6,%ymm10,%ymm14 # tw: to mul by delta -vsubpd %ymm7,%ymm11,%ymm15 # itw: to mul by i.delta -vaddpd %ymm4,%ymm8,%ymm8 -vaddpd %ymm5,%ymm9,%ymm9 -vaddpd %ymm6,%ymm10,%ymm10 -vaddpd %ymm7,%ymm11,%ymm11 -vshufpd $5, %ymm12, %ymm12, %ymm4 -vshufpd $5, %ymm13, %ymm13, %ymm5 -vshufpd $5, %ymm14, %ymm14, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm0,%ymm5 -vmulpd %ymm6,%ymm3,%ymm6 -vmulpd %ymm7,%ymm2,%ymm7 -vfmaddsub231pd %ymm12, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm12) +/- ymm4 -vfmsubadd231pd %ymm13, %ymm1, %ymm5 -vfmaddsub231pd %ymm14, %ymm2, %ymm6 -vfmsubadd231pd %ymm15, %ymm3, %ymm7 - -vperm2f128 $0x20,%ymm6,%ymm10,%ymm12 # ymm4 contains c1,c5 -- x gamma -vperm2f128 $0x20,%ymm7,%ymm11,%ymm13 # ymm5 contains c3,c7 -- x igamma -vperm2f128 $0x31,%ymm6,%ymm10,%ymm14 # ymm6 contains c9,c13 -- x delta -vperm2f128 $0x31,%ymm7,%ymm11,%ymm15 # ymm7 contains c11,c15 -- x idelta -vperm2f128 $0x31,%ymm4,%ymm8,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x31,%ymm5,%ymm9,%ymm11 # ymm11 contains c10,c14 -vperm2f128 $0x20,%ymm4,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm5,%ymm9,%ymm9 # ymm9 contains c2,c6 - - -.third_pass: -vmovupd 64(%rsi),%xmm0 /* gamma */ -vmovupd 80(%rsi),%xmm2 /* delta */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 -vinsertf128 $1, %xmm2, %ymm2, %ymm2 -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vsubpd %ymm9,%ymm8,%ymm4 -vsubpd %ymm11,%ymm10,%ymm5 -vsubpd %ymm13,%ymm12,%ymm6 -vsubpd %ymm15,%ymm14,%ymm7 -vaddpd %ymm9,%ymm8,%ymm8 -vaddpd %ymm11,%ymm10,%ymm10 -vaddpd %ymm13,%ymm12,%ymm12 -vaddpd %ymm15,%ymm14,%ymm14 -vshufpd $5, %ymm4, %ymm4, %ymm9 -vshufpd $5, %ymm5, %ymm5, %ymm11 -vshufpd $5, %ymm6, %ymm6, %ymm13 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm9,%ymm1,%ymm9 -vmulpd %ymm11,%ymm0,%ymm11 -vmulpd %ymm13,%ymm3,%ymm13 -vmulpd %ymm15,%ymm2,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm9 # ymm9 = (ymm0 * ymm4) +/- ymm9 -vfmsubadd231pd %ymm5, %ymm1, %ymm11 -vfmaddsub231pd %ymm6, %ymm2, %ymm13 -vfmsubadd231pd %ymm7, %ymm3, %ymm15 - -.second_pass: -vmovupd 96(%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vsubpd %ymm10,%ymm8,%ymm4 -vsubpd %ymm11,%ymm9,%ymm5 -vsubpd %ymm14,%ymm12,%ymm6 -vsubpd %ymm15,%ymm13,%ymm7 -vaddpd %ymm10,%ymm8,%ymm8 -vaddpd %ymm11,%ymm9,%ymm9 -vaddpd %ymm14,%ymm12,%ymm12 -vaddpd %ymm15,%ymm13,%ymm13 -vshufpd $5, %ymm4, %ymm4, %ymm10 -vshufpd $5, %ymm5, %ymm5, %ymm11 -vshufpd $5, %ymm6, %ymm6, %ymm14 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm10,%ymm1,%ymm10 -vmulpd %ymm11,%ymm1,%ymm11 -vmulpd %ymm14,%ymm0,%ymm14 -vmulpd %ymm15,%ymm0,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm10 # ymm10 = (ymm0 * ymm4) +/- ymm10 -vfmaddsub231pd %ymm5, %ymm0, %ymm11 -vfmsubadd231pd %ymm6, %ymm1, %ymm14 -vfmsubadd231pd %ymm7, %ymm1, %ymm15 - -.first_pass: -vmovupd 112(%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vsubpd %ymm12,%ymm8,%ymm4 -vsubpd %ymm13,%ymm9,%ymm5 -vsubpd %ymm14,%ymm10,%ymm6 -vsubpd %ymm15,%ymm11,%ymm7 -vaddpd %ymm12,%ymm8,%ymm8 -vaddpd %ymm13,%ymm9,%ymm9 -vaddpd %ymm14,%ymm10,%ymm10 -vaddpd %ymm15,%ymm11,%ymm11 -vshufpd $5, %ymm4, %ymm4, %ymm12 -vshufpd $5, %ymm5, %ymm5, %ymm13 -vshufpd $5, %ymm6, %ymm6, %ymm14 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm12,%ymm1,%ymm12 -vmulpd %ymm13,%ymm1,%ymm13 -vmulpd %ymm14,%ymm1,%ymm14 -vmulpd %ymm15,%ymm1,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm12 # ymm12 = (ymm0 * ymm4) +/- ymm12 -vfmaddsub231pd %ymm5, %ymm0, %ymm13 -vfmaddsub231pd %ymm6, %ymm0, %ymm14 -vfmaddsub231pd %ymm7, %ymm0, %ymm15 - -.save_and_return: -vmovupd %ymm8,(%rdi) -vmovupd %ymm9,0x20(%rdi) -vmovupd %ymm10,0x40(%rdi) -vmovupd %ymm11,0x60(%rdi) -vmovupd %ymm12,0x80(%rdi) -vmovupd %ymm13,0xa0(%rdi) -vmovupd %ymm14,0xc0(%rdi) -vmovupd %ymm15,0xe0(%rdi) -ret -.size cplx_ifft16_avx_fma, .-cplx_ifft16_avx_fma -.section .note.GNU-stack,"",@progbits
@@ -1,192 +0,0 @@
- .text - .p2align 4 - .globl cplx_ifft16_avx_fma - .def cplx_ifft16_avx_fma; .scl 2; .type 32; .endef -cplx_ifft16_avx_fma: - - pushq %rdi - pushq %rsi - movq %rcx,%rdi - movq %rdx,%rsi - subq $0x100,%rsp - movdqu %xmm6,(%rsp) - movdqu %xmm7,0x10(%rsp) - movdqu %xmm8,0x20(%rsp) - movdqu %xmm9,0x30(%rsp) - movdqu %xmm10,0x40(%rsp) - movdqu %xmm11,0x50(%rsp) - movdqu %xmm12,0x60(%rsp) - movdqu %xmm13,0x70(%rsp) - movdqu %xmm14,0x80(%rsp) - movdqu %xmm15,0x90(%rsp) - callq cplx_ifft16_avx_fma_amd64 - movdqu (%rsp),%xmm6 - movdqu 0x10(%rsp),%xmm7 - movdqu 0x20(%rsp),%xmm8 - movdqu 0x30(%rsp),%xmm9 - movdqu 0x40(%rsp),%xmm10 - movdqu 0x50(%rsp),%xmm11 - movdqu 0x60(%rsp),%xmm12 - movdqu 0x70(%rsp),%xmm13 - movdqu 0x80(%rsp),%xmm14 - movdqu 0x90(%rsp),%xmm15 - addq $0x100,%rsp - popq %rsi - popq %rdi - retq - -# shifted FFT over X^16-i -# 1st argument (rdi) contains 16 complexes -# 2nd argument (rsi) contains: 8 complexes -# omega,alpha,beta,j.beta,gamma,j.gamma,k.gamma,kj.gamma -# alpha = sqrt(omega), beta = sqrt(alpha), gamma = sqrt(beta) -# j = sqrt(i), k=sqrt(j) - -cplx_ifft16_avx_fma_amd64: -vmovupd (%rdi),%ymm8 # load data into registers %ymm8 -> %ymm15 -vmovupd 0x20(%rdi),%ymm9 -vmovupd 0x40(%rdi),%ymm10 -vmovupd 0x60(%rdi),%ymm11 -vmovupd 0x80(%rdi),%ymm12 -vmovupd 0xa0(%rdi),%ymm13 -vmovupd 0xc0(%rdi),%ymm14 -vmovupd 0xe0(%rdi),%ymm15 - -.fourth_pass: -vmovupd 0(%rsi),%ymm0 /* gamma */ -vmovupd 32(%rsi),%ymm2 /* delta */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vperm2f128 $0x31,%ymm10,%ymm8,%ymm4 # ymm4 contains c1,c5 -vperm2f128 $0x31,%ymm11,%ymm9,%ymm5 # ymm5 contains c3,c7 -vperm2f128 $0x31,%ymm14,%ymm12,%ymm6 # ymm6 contains c9,c13 -vperm2f128 $0x31,%ymm15,%ymm13,%ymm7 # ymm7 contains c11,c15 -vperm2f128 $0x20,%ymm10,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm11,%ymm9,%ymm9 # ymm9 contains c2,c6 -vperm2f128 $0x20,%ymm14,%ymm12,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x20,%ymm15,%ymm13,%ymm11 # ymm11 contains c10,c14 -vsubpd %ymm4,%ymm8,%ymm12 # tw: to mul by gamma -vsubpd %ymm5,%ymm9,%ymm13 # itw: to mul by i.gamma -vsubpd %ymm6,%ymm10,%ymm14 # tw: to mul by delta -vsubpd %ymm7,%ymm11,%ymm15 # itw: to mul by i.delta -vaddpd %ymm4,%ymm8,%ymm8 -vaddpd %ymm5,%ymm9,%ymm9 -vaddpd %ymm6,%ymm10,%ymm10 -vaddpd %ymm7,%ymm11,%ymm11 -vshufpd $5, %ymm12, %ymm12, %ymm4 -vshufpd $5, %ymm13, %ymm13, %ymm5 -vshufpd $5, %ymm14, %ymm14, %ymm6 -vshufpd $5, %ymm15, %ymm15, %ymm7 -vmulpd %ymm4,%ymm1,%ymm4 -vmulpd %ymm5,%ymm0,%ymm5 -vmulpd %ymm6,%ymm3,%ymm6 -vmulpd %ymm7,%ymm2,%ymm7 -vfmaddsub231pd %ymm12, %ymm0, %ymm4 # ymm4 = (ymm0 * ymm12) +/- ymm4 -vfmsubadd231pd %ymm13, %ymm1, %ymm5 -vfmaddsub231pd %ymm14, %ymm2, %ymm6 -vfmsubadd231pd %ymm15, %ymm3, %ymm7 - -vperm2f128 $0x20,%ymm6,%ymm10,%ymm12 # ymm4 contains c1,c5 -- x gamma -vperm2f128 $0x20,%ymm7,%ymm11,%ymm13 # ymm5 contains c3,c7 -- x igamma -vperm2f128 $0x31,%ymm6,%ymm10,%ymm14 # ymm6 contains c9,c13 -- x delta -vperm2f128 $0x31,%ymm7,%ymm11,%ymm15 # ymm7 contains c11,c15 -- x idelta -vperm2f128 $0x31,%ymm4,%ymm8,%ymm10 # ymm10 contains c8,c12 -vperm2f128 $0x31,%ymm5,%ymm9,%ymm11 # ymm11 contains c10,c14 -vperm2f128 $0x20,%ymm4,%ymm8,%ymm8 # ymm8 contains c0,c4 -vperm2f128 $0x20,%ymm5,%ymm9,%ymm9 # ymm9 contains c2,c6 - - -.third_pass: -vmovupd 64(%rsi),%xmm0 /* gamma */ -vmovupd 80(%rsi),%xmm2 /* delta */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 -vinsertf128 $1, %xmm2, %ymm2, %ymm2 -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: gama.iiii */ -vshufpd $15, %ymm2, %ymm2, %ymm3 /* ymm3: delta.iiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: gama.rrrr */ -vshufpd $0, %ymm2, %ymm2, %ymm2 /* ymm2: delta.rrrr */ -vsubpd %ymm9,%ymm8,%ymm4 -vsubpd %ymm11,%ymm10,%ymm5 -vsubpd %ymm13,%ymm12,%ymm6 -vsubpd %ymm15,%ymm14,%ymm7 -vaddpd %ymm9,%ymm8,%ymm8 -vaddpd %ymm11,%ymm10,%ymm10 -vaddpd %ymm13,%ymm12,%ymm12 -vaddpd %ymm15,%ymm14,%ymm14 -vshufpd $5, %ymm4, %ymm4, %ymm9 -vshufpd $5, %ymm5, %ymm5, %ymm11 -vshufpd $5, %ymm6, %ymm6, %ymm13 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm9,%ymm1,%ymm9 -vmulpd %ymm11,%ymm0,%ymm11 -vmulpd %ymm13,%ymm3,%ymm13 -vmulpd %ymm15,%ymm2,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm9 # ymm9 = (ymm0 * ymm4) +/- ymm9 -vfmsubadd231pd %ymm5, %ymm1, %ymm11 -vfmaddsub231pd %ymm6, %ymm2, %ymm13 -vfmsubadd231pd %ymm7, %ymm3, %ymm15 - -.second_pass: -vmovupd 96(%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vsubpd %ymm10,%ymm8,%ymm4 -vsubpd %ymm11,%ymm9,%ymm5 -vsubpd %ymm14,%ymm12,%ymm6 -vsubpd %ymm15,%ymm13,%ymm7 -vaddpd %ymm10,%ymm8,%ymm8 -vaddpd %ymm11,%ymm9,%ymm9 -vaddpd %ymm14,%ymm12,%ymm12 -vaddpd %ymm15,%ymm13,%ymm13 -vshufpd $5, %ymm4, %ymm4, %ymm10 -vshufpd $5, %ymm5, %ymm5, %ymm11 -vshufpd $5, %ymm6, %ymm6, %ymm14 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm10,%ymm1,%ymm10 -vmulpd %ymm11,%ymm1,%ymm11 -vmulpd %ymm14,%ymm0,%ymm14 -vmulpd %ymm15,%ymm0,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm10 # ymm10 = (ymm0 * ymm4) +/- ymm10 -vfmaddsub231pd %ymm5, %ymm0, %ymm11 -vfmsubadd231pd %ymm6, %ymm1, %ymm14 -vfmsubadd231pd %ymm7, %ymm1, %ymm15 - -.first_pass: -vmovupd 112(%rsi),%xmm0 /* omri */ -vinsertf128 $1, %xmm0, %ymm0, %ymm0 /* omriri */ -vshufpd $15, %ymm0, %ymm0, %ymm1 /* ymm1: omiiii */ -vshufpd $0, %ymm0, %ymm0, %ymm0 /* ymm0: omrrrr */ -vsubpd %ymm12,%ymm8,%ymm4 -vsubpd %ymm13,%ymm9,%ymm5 -vsubpd %ymm14,%ymm10,%ymm6 -vsubpd %ymm15,%ymm11,%ymm7 -vaddpd %ymm12,%ymm8,%ymm8 -vaddpd %ymm13,%ymm9,%ymm9 -vaddpd %ymm14,%ymm10,%ymm10 -vaddpd %ymm15,%ymm11,%ymm11 -vshufpd $5, %ymm4, %ymm4, %ymm12 -vshufpd $5, %ymm5, %ymm5, %ymm13 -vshufpd $5, %ymm6, %ymm6, %ymm14 -vshufpd $5, %ymm7, %ymm7, %ymm15 -vmulpd %ymm12,%ymm1,%ymm12 -vmulpd %ymm13,%ymm1,%ymm13 -vmulpd %ymm14,%ymm1,%ymm14 -vmulpd %ymm15,%ymm1,%ymm15 -vfmaddsub231pd %ymm4, %ymm0, %ymm12 # ymm12 = (ymm0 * ymm4) +/- ymm12 -vfmaddsub231pd %ymm5, %ymm0, %ymm13 -vfmaddsub231pd %ymm6, %ymm0, %ymm14 -vfmaddsub231pd %ymm7, %ymm0, %ymm15 - -.save_and_return: -vmovupd %ymm8,(%rdi) -vmovupd %ymm9,0x20(%rdi) -vmovupd %ymm10,0x40(%rdi) -vmovupd %ymm11,0x60(%rdi) -vmovupd %ymm12,0x80(%rdi) -vmovupd %ymm13,0xa0(%rdi) -vmovupd %ymm14,0xc0(%rdi) -vmovupd %ymm15,0xe0(%rdi) -ret
@@ -1,267 +0,0 @@
-#include <immintrin.h> -#include <math.h> -#include <string.h> - -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -typedef double D4MEM[4]; -typedef double D2MEM[2]; - -/** - * @brief complex ifft via bfs strategy (for m between 2 and 8) - * @param dat the data to run the algorithm on - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -void cplx_ifft_avx2_fma_bfs_2(D4MEM* dat, const D2MEM** omga, uint32_t m) { - double* data = (double*)dat; - D4MEM* const finaldd = (D4MEM*)(data + 2 * m); - { - // loop with h = 1 - // we do not do any particular optimization in this loop, - // since this function is only called for small dimensions - D4MEM* dd = (D4MEM*)data; - do { - /* - BEGIN_TEMPLATE - const __m256d ab% = _mm256_loadu_pd(dd[0+2*%]); - const __m256d cd% = _mm256_loadu_pd(dd[1+2*%]); - const __m256d ac% = _mm256_permute2f128_pd(ab%, cd%, 0b100000); - const __m256d bd% = _mm256_permute2f128_pd(ab%, cd%, 0b110001); - const __m256d sum% = _mm256_add_pd(ac%, bd%); - const __m256d diff% = _mm256_sub_pd(ac%, bd%); - const __m256d diffbar% = _mm256_shuffle_pd(diff%, diff%, 5); - const __m256d om% = _mm256_load_pd((*omg)[0+%]); - const __m256d omre% = _mm256_unpacklo_pd(om%, om%); - const __m256d omim% = _mm256_unpackhi_pd(om%, om%); - const __m256d t1% = _mm256_mul_pd(diffbar%, omim%); - const __m256d t2% = _mm256_fmaddsub_pd(diff%, omre%, t1%); - const __m256d newab% = _mm256_permute2f128_pd(sum%, t2%, 0b100000); - const __m256d newcd% = _mm256_permute2f128_pd(sum%, t2%, 0b110001); - _mm256_storeu_pd(dd[0+2*%], newab%); - _mm256_storeu_pd(dd[1+2*%], newcd%); - dd += 2*@; - *omg += 2*@; - END_TEMPLATE - */ - // BEGIN_INTERLEAVE 1 - const __m256d ab0 = _mm256_loadu_pd(dd[0 + 2 * 0]); - const __m256d cd0 = _mm256_loadu_pd(dd[1 + 2 * 0]); - const __m256d ac0 = _mm256_permute2f128_pd(ab0, cd0, 0b100000); - const __m256d bd0 = _mm256_permute2f128_pd(ab0, cd0, 0b110001); - const __m256d sum0 = _mm256_add_pd(ac0, bd0); - const __m256d diff0 = _mm256_sub_pd(ac0, bd0); - const __m256d diffbar0 = _mm256_shuffle_pd(diff0, diff0, 5); - const __m256d om0 = _mm256_load_pd((*omga)[0 + 0]); - const __m256d omre0 = _mm256_unpacklo_pd(om0, om0); - const __m256d omim0 = _mm256_unpackhi_pd(om0, om0); - const __m256d t10 = _mm256_mul_pd(diffbar0, omim0); - const __m256d t20 = _mm256_fmaddsub_pd(diff0, omre0, t10); - const __m256d newab0 = _mm256_permute2f128_pd(sum0, t20, 0b100000); - const __m256d newcd0 = _mm256_permute2f128_pd(sum0, t20, 0b110001); - _mm256_storeu_pd(dd[0 + 2 * 0], newab0); - _mm256_storeu_pd(dd[1 + 2 * 0], newcd0); - dd += 2 * 1; - *omga += 2 * 1; - // END_INTERLEAVE - } while (dd < finaldd); -#if 0 - printf("c after first: "); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",ddata[ii][0],ddata[ii][1]); - } - printf("\n"); -#endif - } - // general case - const uint32_t ms2 = m >> 1; - for (uint32_t _2nblock = 2; _2nblock <= ms2; _2nblock <<= 1) { - // _2nblock = h in ref code - uint32_t nblock = _2nblock >> 1; // =h/2 in ref code - D4MEM* dd = (D4MEM*)data; - do { - const __m256d om = _mm256_load_pd((*omga)[0]); - const __m256d omre = _mm256_unpacklo_pd(om, om); - const __m256d omim = _mm256_addsub_pd(_mm256_setzero_pd(), _mm256_unpackhi_pd(om, om)); - D4MEM* const ddend = (dd + nblock); - D4MEM* ddmid = ddend; - do { - const __m256d a = _mm256_loadu_pd(dd[0]); - const __m256d b = _mm256_loadu_pd(ddmid[0]); - const __m256d newa = _mm256_add_pd(a, b); - _mm256_storeu_pd(dd[0], newa); - const __m256d diff = _mm256_sub_pd(a, b); - const __m256d t1 = _mm256_mul_pd(diff, omre); - const __m256d bardiff = _mm256_shuffle_pd(diff, diff, 5); - const __m256d t2 = _mm256_fmadd_pd(bardiff, omim, t1); - _mm256_storeu_pd(ddmid[0], t2); - dd += 1; - ddmid += 1; - } while (dd < ddend); - dd += nblock; - *omga += 2; - } while (dd < finaldd); - } -} - -/** - * @brief complex fft via bfs strategy (for m >= 16) - * @param dat the data to run the algorithm on - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -void cplx_ifft_avx2_fma_bfs_16(D4MEM* dat, const D2MEM** omga, uint32_t m) { - double* data = (double*)dat; - D4MEM* const finaldd = (D4MEM*)(data + 2 * m); - // base iteration when h = _2nblock == 8 - { - D4MEM* dd = (D4MEM*)data; - do { - cplx_ifft16_avx_fma(dd, *omga); - dd += 8; - *omga += 8; - } while (dd < finaldd); - } - // general case - const uint32_t log2m = _mm_popcnt_u32(m - 1); //_popcnt32(m-1); //log2(m); - uint32_t h = 16; - if (log2m % 2 == 1) { - uint32_t nblock = h >> 1; // =h/2 in ref code - D4MEM* dd = (D4MEM*)data; - do { - const __m128d om1 = _mm_loadu_pd((*omga)[0]); - const __m256d om = _mm256_set_m128d(om1, om1); - const __m256d omre = _mm256_unpacklo_pd(om, om); - const __m256d omim = _mm256_addsub_pd(_mm256_setzero_pd(), _mm256_unpackhi_pd(om, om)); - D4MEM* const ddend = (dd + nblock); - D4MEM* ddmid = ddend; - do { - const __m256d a = _mm256_loadu_pd(dd[0]); - const __m256d b = _mm256_loadu_pd(ddmid[0]); - const __m256d newa = _mm256_add_pd(a, b); - _mm256_storeu_pd(dd[0], newa); - const __m256d diff = _mm256_sub_pd(a, b); - const __m256d t1 = _mm256_mul_pd(diff, omre); - const __m256d bardiff = _mm256_shuffle_pd(diff, diff, 5); - const __m256d t2 = _mm256_fmadd_pd(bardiff, omim, t1); - _mm256_storeu_pd(ddmid[0], t2); - dd += 1; - ddmid += 1; - } while (dd < ddend); - dd += nblock; - *omga += 1; - } while (dd < finaldd); - h = 32; - } - for (; h < m; h <<= 2) { - // _2nblock = h in ref code - uint32_t nblock = h >> 1; // =h/2 in ref code - D4MEM* dd0 = (D4MEM*)data; - do { - const __m128d om1 = _mm_loadu_pd((*omga)[0]); - const __m128d al1 = _mm_loadu_pd((*omga)[1]); - const __m256d om = _mm256_set_m128d(om1, om1); - const __m256d al = _mm256_set_m128d(al1, al1); - const __m256d omre = _mm256_unpacklo_pd(om, om); - const __m256d omim = _mm256_unpackhi_pd(om, om); - const __m256d alre = _mm256_unpacklo_pd(al, al); - const __m256d alim = _mm256_unpackhi_pd(al, al); - D4MEM* const ddend = (dd0 + nblock); - D4MEM* dd1 = ddend; - D4MEM* dd2 = dd1 + nblock; - D4MEM* dd3 = dd2 + nblock; - do { - __m256d u0 = _mm256_loadu_pd(dd0[0]); - __m256d u1 = _mm256_loadu_pd(dd1[0]); - __m256d u2 = _mm256_loadu_pd(dd2[0]); - __m256d u3 = _mm256_loadu_pd(dd3[0]); - __m256d u4 = _mm256_add_pd(u0, u1); - __m256d u5 = _mm256_sub_pd(u0, u1); - __m256d u6 = _mm256_add_pd(u2, u3); - __m256d u7 = _mm256_sub_pd(u2, u3); - u0 = _mm256_shuffle_pd(u5, u5, 5); - u2 = _mm256_shuffle_pd(u7, u7, 5); - u1 = _mm256_mul_pd(u0, omim); - u3 = _mm256_mul_pd(u2, omre); - u5 = _mm256_fmaddsub_pd(u5, omre, u1); - u7 = _mm256_fmsubadd_pd(u7, omim, u3); - ////// - u0 = _mm256_add_pd(u4, u6); - u1 = _mm256_add_pd(u5, u7); - u2 = _mm256_sub_pd(u4, u6); - u3 = _mm256_sub_pd(u5, u7); - u4 = _mm256_shuffle_pd(u2, u2, 5); - u5 = _mm256_shuffle_pd(u3, u3, 5); - u6 = _mm256_mul_pd(u4, alim); - u7 = _mm256_mul_pd(u5, alim); - u2 = _mm256_fmaddsub_pd(u2, alre, u6); - u3 = _mm256_fmaddsub_pd(u3, alre, u7); - /////// - _mm256_storeu_pd(dd0[0], u0); - _mm256_storeu_pd(dd1[0], u1); - _mm256_storeu_pd(dd2[0], u2); - _mm256_storeu_pd(dd3[0], u3); - dd0 += 1; - dd1 += 1; - dd2 += 1; - dd3 += 1; - } while (dd0 < ddend); - dd0 += 3 * nblock; - *omga += 2; - } while (dd0 < finaldd); - } -} - -/** - * @brief complex ifft via dfs recursion (for m >= 16) - * @param dat the data to run the algorithm on - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -void cplx_ifft_avx2_fma_rec_16(D4MEM* dat, const D2MEM** omga, uint32_t m) { - if (m <= 8) return cplx_ifft_avx2_fma_bfs_2(dat, omga, m); - if (m <= 2048) return cplx_ifft_avx2_fma_bfs_16(dat, omga, m); - const uint32_t _2nblock = m >> 1; // = h in ref code - const uint32_t nblock = _2nblock >> 1; // =h/2 in ref code - cplx_ifft_avx2_fma_rec_16(dat, omga, _2nblock); - cplx_ifft_avx2_fma_rec_16(dat + nblock, omga, _2nblock); - { - // final iteration - D4MEM* dd = dat; - const __m256d om = _mm256_load_pd((*omga)[0]); - const __m256d omre = _mm256_unpacklo_pd(om, om); - const __m256d omim = _mm256_addsub_pd(_mm256_setzero_pd(), _mm256_unpackhi_pd(om, om)); - D4MEM* const ddend = (dd + nblock); - D4MEM* ddmid = ddend; - do { - const __m256d a = _mm256_loadu_pd(dd[0]); - const __m256d b = _mm256_loadu_pd(ddmid[0]); - const __m256d newa = _mm256_add_pd(a, b); - _mm256_storeu_pd(dd[0], newa); - const __m256d diff = _mm256_sub_pd(a, b); - const __m256d t1 = _mm256_mul_pd(diff, omre); - const __m256d bardiff = _mm256_shuffle_pd(diff, diff, 5); - const __m256d t2 = _mm256_fmadd_pd(bardiff, omim, t1); - _mm256_storeu_pd(ddmid[0], t2); - dd += 1; - ddmid += 1; - } while (dd < ddend); - *omga += 2; - } -} - -/** - * @brief complex ifft via best strategy (for m>=1) - * @param dat the data to run the algorithm on: m complex numbers - * @param omg precomputed tables (must have been filled with fill_omega) - * @param m ring dimension of the FFT (modulo X^m-i) - */ -EXPORT void cplx_ifft_avx2_fma(const CPLX_IFFT_PRECOMP* precomp, void* d) { - const uint32_t m = precomp->m; - const D2MEM* omg = (D2MEM*)precomp->powomegas; - if (m <= 1) return; - if (m <= 8) return cplx_ifft_avx2_fma_bfs_2(d, &omg, m); - if (m <= 2048) return cplx_ifft_avx2_fma_bfs_16(d, &omg, m); - cplx_ifft_avx2_fma_rec_16(d, &omg, m); -}
@@ -1,312 +0,0 @@
-#include <memory.h> - -#include "../commons_private.h" -#include "cplx_fft.h" -#include "cplx_fft_internal.h" -#include "cplx_fft_private.h" - -/** @brief (a,b) <- (a+b,omegabar.(a-b)) */ -void invctwiddle(CPLX a, CPLX b, const CPLX ombar) { - double diffre = a[0] - b[0]; - double diffim = a[1] - b[1]; - a[0] = a[0] + b[0]; - a[1] = a[1] + b[1]; - b[0] = diffre * ombar[0] - diffim * ombar[1]; - b[1] = diffre * ombar[1] + diffim * ombar[0]; -} - -/** @brief (a,b) <- (a+b,-i.omegabar(a-b)) */ -void invcitwiddle(CPLX a, CPLX b, const CPLX ombar) { - double diffre = a[0] - b[0]; - double diffim = a[1] - b[1]; - a[0] = a[0] + b[0]; - a[1] = a[1] + b[1]; - //-i(x+iy)=-ix+y - b[0] = diffre * ombar[1] + diffim * ombar[0]; - b[1] = -diffre * ombar[0] + diffim * ombar[1]; -} - -/** @brief exp(-i.2pi.x) */ -void cplx_set_e2pimx(CPLX res, double x) { - res[0] = m_accurate_cos(2 * M_PI * x); - res[1] = -m_accurate_sin(2 * M_PI * x); -} -/** - * @brief this computes the sequence: 0,1/2,1/4,3/4,1/8,5/8,3/8,7/8,... - * essentially: the bits of (i+1) in lsb order on the basis (1/2^k) mod 1*/ -double fracrevbits(uint32_t i); -/** @brief fft modulo X^m-exp(i.2pi.entry+pwr) -- reference code */ -void cplx_ifft_naive(const uint32_t m, const double entry_pwr, CPLX* data) { - if (m == 1) return; - const double pom = entry_pwr / 2.; - const uint32_t h = m / 2; - CPLX cpom; - cplx_set_e2pimx(cpom, pom); - // do the recursive calls - cplx_ifft_naive(h, pom, data); - cplx_ifft_naive(h, pom + 0.5, data + h); - // apply the inverse twiddle factors - for (uint64_t i = 0; i < h; ++i) { - invctwiddle(data[i], data[i + h], cpom); - } -} - -void cplx_ifft16_precomp(const double entry_pwr, CPLX** omg) { - static const double j_pow = 1. / 8.; - static const double k_pow = 1. / 16.; - const double pom = entry_pwr / 2.; - const double pom_2 = entry_pwr / 4.; - const double pom_4 = entry_pwr / 8.; - const double pom_8 = entry_pwr / 16.; - cplx_set_e2pimx((*omg)[0], pom_8); - cplx_set_e2pimx((*omg)[1], pom_8 + j_pow); - cplx_set_e2pimx((*omg)[2], pom_8 + k_pow); - cplx_set_e2pimx((*omg)[3], pom_8 + j_pow + k_pow); - cplx_set_e2pimx((*omg)[4], pom_4); - cplx_set_e2pimx((*omg)[5], pom_4 + j_pow); - cplx_set_e2pimx((*omg)[6], pom_2); - cplx_set_e2pimx((*omg)[7], pom); - *omg += 8; -} - -/** - * @brief iFFT modulo X^16-omega^2 (in registers) - * @param data contains 16 complexes - * @param omegabar 8 complexes in this order: - * gammabar,jb.gammabar,kb.gammabar,kbjb.gammabar, - * betabar,jb.betabar,alphabar,omegabar - * alpha = sqrt(omega), beta = sqrt(alpha), gamma = sqrt(beta) - * jb = sqrt(ib), kb=sqrt(jb) - */ -void cplx_ifft16_ref(void* data, const void* omegabar) { - CPLX* d = data; - const CPLX* om = omegabar; - // fourth pass inverse - invctwiddle(d[0], d[1], om[0]); - invcitwiddle(d[2], d[3], om[0]); - invctwiddle(d[4], d[5], om[1]); - invcitwiddle(d[6], d[7], om[1]); - invctwiddle(d[8], d[9], om[2]); - invcitwiddle(d[10], d[11], om[2]); - invctwiddle(d[12], d[13], om[3]); - invcitwiddle(d[14], d[15], om[3]); - // third pass inverse - invctwiddle(d[0], d[2], om[4]); - invctwiddle(d[1], d[3], om[4]); - invcitwiddle(d[4], d[6], om[4]); - invcitwiddle(d[5], d[7], om[4]); - invctwiddle(d[8], d[10], om[5]); - invctwiddle(d[9], d[11], om[5]); - invcitwiddle(d[12], d[14], om[5]); - invcitwiddle(d[13], d[15], om[5]); - // second pass inverse - invctwiddle(d[0], d[4], om[6]); - invctwiddle(d[1], d[5], om[6]); - invctwiddle(d[2], d[6], om[6]); - invctwiddle(d[3], d[7], om[6]); - invcitwiddle(d[8], d[12], om[6]); - invcitwiddle(d[9], d[13], om[6]); - invcitwiddle(d[10], d[14], om[6]); - invcitwiddle(d[11], d[15], om[6]); - // first pass - for (uint64_t i = 0; i < 8; ++i) { - invctwiddle(d[0 + i], d[8 + i], om[7]); - } -} - -void cplx_ifft_ref_bfs_2(CPLX* dat, const CPLX** omg, uint32_t m) { - CPLX* const dend = dat + m; - for (CPLX* d = dat; d < dend; d += 2) { - split_fft_last_ref(d, (*omg)[0]); - *omg += 1; - } -#if 0 - printf("after first: "); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif - int32_t Ms2 = m / 2; - for (int32_t h = 2; h <= Ms2; h <<= 1) { - for (CPLX* d = dat; d < dend; d += 2 * h) { - if (memcmp((*omg)[0], (*omg)[1], 8) != 0) abort(); - cplx_split_fft_ref(h, d, **omg); - *omg += 2; - } -#if 0 - printf("after split %d: ", h); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif - } -} - -void cplx_ifft_ref_bfs_16(CPLX* dat, const CPLX** omg, uint32_t m) { - const uint64_t log2m = log2(m); - CPLX* const dend = dat + m; - // h=1,2,4,8 use the 16-dim macroblock - for (CPLX* d = dat; d < dend; d += 16) { - cplx_ifft16_ref(d, *omg); - *omg += 8; - } -#if 0 - printf("after first: "); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif - int32_t h = 16; - if (log2m % 2 != 0) { - // if parity needs it, uses one regular twiddle - for (CPLX* d = dat; d < dend; d += 2 * h) { - cplx_split_fft_ref(h, d, **omg); - *omg += 1; - } - h = 32; - } - // h=16,...,2*floor(Ms2/2) use the bitwiddle - for (; h < m; h <<= 2) { - for (CPLX* d = dat; d < dend; d += 4 * h) { - cplx_bisplit_fft_ref(h, d, *omg); - *omg += 2; - } -#if 0 - printf("after split %d: ", h); - for (uint64_t ii=0; ii<nn/2; ++ii) { - printf("%.6lf %.6lf ",data[ii][0],data[ii][1]); - } - printf("\n"); -#endif - } -} - -void fill_cplx_ifft_omegas_bfs_16(const double entry_pwr, CPLX** omg, uint32_t m) { - const uint64_t log2m = log2(m); - double pwr = entry_pwr * 16. / m; - { - // h=8 - for (uint32_t i = 0; i < m / 16; i++) { - cplx_ifft16_precomp(pwr + fracrevbits(i), omg); - } - } - int32_t h = 16; - if (log2m % 2 != 0) { - // if parity needs it, uses one regular twiddle - for (uint32_t i = 0; i < m / (2 * h); i++) { - cplx_set_e2pimx(omg[0][0], pwr + fracrevbits(i) / 2.); - *omg += 1; - } - pwr *= 2.; - h = 32; - } - for (; h < m; h <<= 2) { - for (uint32_t i = 0; i < m / (2 * h); i += 2) { - cplx_set_e2pimx(omg[0][0], pwr + fracrevbits(i) / 2.); - cplx_set_e2pimx(omg[0][1], 2. * pwr + fracrevbits(i)); - *omg += 2; - } - pwr *= 4.; - } -} - -void fill_cplx_ifft_omegas_bfs_2(const double entry_pwr, CPLX** omg, uint32_t m) { - double pom = entry_pwr / m; - { - // h=1 - for (uint32_t i = 0; i < m / 2; i++) { - cplx_set_e2pimx((*omg)[0], pom + fracrevbits(i) / 2.); - *omg += 1; // optim function reads by 1 - } - } - for (int32_t h = 2; h <= m / 2; h <<= 1) { - pom *= 2; - for (uint32_t i = 0; i < m / (2 * h); i++) { - cplx_set_e2pimx(omg[0][0], pom + fracrevbits(i) / 2.); - cplx_set(omg[0][1], omg[0][0]); - *omg += 2; - } - } -} - -void fill_cplx_ifft_omegas_rec_16(const double entry_pwr, CPLX** omg, uint32_t m) { - if (m == 1) return; - if (m <= 8) return fill_cplx_ifft_omegas_bfs_2(entry_pwr, omg, m); - if (m <= 2048) return fill_cplx_ifft_omegas_bfs_16(entry_pwr, omg, m); - double pom = entry_pwr / 2.; - uint32_t h = m / 2; - fill_cplx_ifft_omegas_rec_16(pom, omg, h); - fill_cplx_ifft_omegas_rec_16(pom + 0.5, omg, h); - cplx_set_e2pimx((*omg)[0], pom); - cplx_set((*omg)[1], (*omg)[0]); - *omg += 2; -} - -void cplx_ifft_ref_rec_16(CPLX* dat, const CPLX** omg, uint32_t m) { - if (m == 1) return; - if (m <= 8) return cplx_ifft_ref_bfs_2(dat, omg, m); - if (m <= 2048) return cplx_ifft_ref_bfs_16(dat, omg, m); - const uint32_t h = m / 2; - cplx_ifft_ref_rec_16(dat, omg, h); - cplx_ifft_ref_rec_16(dat + h, omg, h); - if (memcmp((*omg)[0], (*omg)[1], 8) != 0) abort(); - cplx_split_fft_ref(h, dat, **omg); - *omg += 2; -} - -EXPORT void cplx_ifft_ref(const CPLX_IFFT_PRECOMP* precomp, void* d) { - CPLX* data = (CPLX*)d; - const int32_t m = precomp->m; - const CPLX* omg = (CPLX*)precomp->powomegas; - if (m == 1) return; - if (m <= 8) return cplx_ifft_ref_bfs_2(data, &omg, m); - if (m <= 2048) return cplx_ifft_ref_bfs_16(data, &omg, m); - cplx_ifft_ref_rec_16(data, &omg, m); -} - -EXPORT CPLX_IFFT_PRECOMP* new_cplx_ifft_precomp(uint32_t m, uint32_t num_buffers) { - const uint64_t OMG_SPACE = ceilto64b(2 * m * sizeof(CPLX)); - const uint64_t BUF_SIZE = ceilto64b(m * sizeof(CPLX)); - void* reps = malloc(sizeof(CPLX_IFFT_PRECOMP) + 63 // padding - + OMG_SPACE // tables - + num_buffers * BUF_SIZE // buffers - ); - uint64_t aligned_addr = ceilto64b((uint64_t)reps + sizeof(CPLX_IFFT_PRECOMP)); - CPLX_IFFT_PRECOMP* r = (CPLX_IFFT_PRECOMP*)reps; - r->m = m; - r->buf_size = BUF_SIZE; - r->powomegas = (double*)aligned_addr; - r->aligned_buffers = (void*)(aligned_addr + OMG_SPACE); - // fill in powomegas - CPLX* omg = (CPLX*)r->powomegas; - fill_cplx_ifft_omegas_rec_16(0.25, &omg, m); - if (((uint64_t)omg) - aligned_addr > OMG_SPACE) abort(); - { - if (m <= 4) { - // currently, we do not have any acceletated - // implementation for m<=4 - r->function = cplx_ifft_ref; - } else if (CPU_SUPPORTS("fma")) { - r->function = cplx_ifft_avx2_fma; - } else { - r->function = cplx_ifft_ref; - } - } - return reps; -} - -EXPORT void* cplx_ifft_precomp_get_buffer(const CPLX_IFFT_PRECOMP* itables, uint32_t buffer_index) { - return (uint8_t*)itables->aligned_buffers + buffer_index * itables->buf_size; -} - -EXPORT void cplx_ifft(const CPLX_IFFT_PRECOMP* itables, void* data) { itables->function(itables, data); } - -EXPORT void cplx_ifft_simple(uint32_t m, void* data) { - static CPLX_IFFT_PRECOMP* p[31] = {0}; - CPLX_IFFT_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_cplx_ifft_precomp(m, 0); - (*f)->function(*f, data); -}
@@ -1,136 +0,0 @@
-/* - * This file is extracted from the implementation of the FFT on Arm64/Neon - * available in https://github.com/cothan/Falcon-Arm (neon/macrof.h). - * ============================================================================= - * Copyright (c) 2022 by Cryptographic Engineering Research Group (CERG) - * ECE Department, George Mason University - * Fairfax, VA, U.S.A. - * @author: Duc Tri Nguyen dnguye69@gmu.edu, cothannguyen@gmail.com - * Licensed under the Apache License, Version 2.0 (the "License"); - * ============================================================================= - * - * This 64-bit Floating point NEON macro x1 has not been modified and is provided as is. - */ - -#ifndef MACROF_H -#define MACROF_H - -#include <arm_neon.h> - -// c <= addr x1 -#define vload(c, addr) c = vld1q_f64(addr); -// c <= addr interleave 2 -#define vload2(c, addr) c = vld2q_f64(addr); -// c <= addr interleave 4 -#define vload4(c, addr) c = vld4q_f64(addr); - -#define vstore(addr, c) vst1q_f64(addr, c); -// addr <= c -#define vstore2(addr, c) vst2q_f64(addr, c); -// addr <= c -#define vstore4(addr, c) vst4q_f64(addr, c); - -// c <= addr x2 -#define vloadx2(c, addr) c = vld1q_f64_x2(addr); -// c <= addr x3 -#define vloadx3(c, addr) c = vld1q_f64_x3(addr); - -// addr <= c -#define vstorex2(addr, c) vst1q_f64_x2(addr, c); - -// c = a - b -#define vfsub(c, a, b) c = vsubq_f64(a, b); - -// c = a + b -#define vfadd(c, a, b) c = vaddq_f64(a, b); - -// c = a * b -#define vfmul(c, a, b) c = vmulq_f64(a, b); - -// c = a * n (n is constant) -#define vfmuln(c, a, n) c = vmulq_n_f64(a, n); - -// Swap from a|b to b|a -#define vswap(c, a) c = vextq_f64(a, a, 1); - -// c = a * b[i] -#define vfmul_lane(c, a, b, i) c = vmulq_laneq_f64(a, b, i); - -// c = 1/a -#define vfinv(c, a) c = vdivq_f64(vdupq_n_f64(1.0), a); - -// c = -a -#define vfneg(c, a) c = vnegq_f64(a); - -#define transpose_f64(a, b, t, ia, ib, it) \ - t.val[it] = a.val[ia]; \ - a.val[ia] = vzip1q_f64(a.val[ia], b.val[ib]); \ - b.val[ib] = vzip2q_f64(t.val[it], b.val[ib]); - -/* - * c = a + jb - * c[0] = a[0] - b[1] - * c[1] = a[1] + b[0] - */ -#define vfcaddj(c, a, b) c = vcaddq_rot90_f64(a, b); - -/* - * c = a - jb - * c[0] = a[0] + b[1] - * c[1] = a[1] - b[0] - */ -#define vfcsubj(c, a, b) c = vcaddq_rot270_f64(a, b); - -// c[0] = c[0] + b[0]*a[0], c[1] = c[1] + b[1]*a[0] -#define vfcmla(c, a, b) c = vcmlaq_f64(c, a, b); - -// c[0] = c[0] - b[1]*a[1], c[1] = c[1] + b[0]*a[1] -#define vfcmla_90(c, a, b) c = vcmlaq_rot90_f64(c, a, b); - -// c[0] = c[0] - b[0]*a[0], c[1] = c[1] - b[1]*a[0] -#define vfcmla_180(c, a, b) c = vcmlaq_rot180_f64(c, a, b); - -// c[0] = c[0] + b[1]*a[1], c[1] = c[1] - b[0]*a[1] -#define vfcmla_270(c, a, b) c = vcmlaq_rot270_f64(c, a, b); - -/* - * Complex MUL: c = a*b - * c[0] = a[0]*b[0] - a[1]*b[1] - * c[1] = a[0]*b[1] + a[1]*b[0] - */ -#define FPC_CMUL(c, a, b) \ - c = vmulq_laneq_f64(b, a, 0); \ - c = vcmlaq_rot90_f64(c, a, b); - -/* - * Complex MUL: c = a * conjugate(b) = a * (b[0], -b[1]) - * c[0] = b[0]*a[0] + b[1]*a[1] - * c[1] = + b[0]*a[1] - b[1]*a[0] - */ -#define FPC_CMUL_CONJ(c, a, b) \ - c = vmulq_laneq_f64(a, b, 0); \ - c = vcmlaq_rot270_f64(c, b, a); - -#if FMA == 1 -// d = c + a *b -#define vfmla(d, c, a, b) d = vfmaq_f64(c, a, b); -// d = c - a * b -#define vfmls(d, c, a, b) d = vfmsq_f64(c, a, b); -// d = c + a * b[i] -#define vfmla_lane(d, c, a, b, i) d = vfmaq_laneq_f64(c, a, b, i); -// d = c - a * b[i] -#define vfmls_lane(d, c, a, b, i) d = vfmsq_laneq_f64(c, a, b, i); - -#else -// d = c + a *b -#define vfmla(d, c, a, b) d = vaddq_f64(c, vmulq_f64(a, b)); -// d = c - a *b -#define vfmls(d, c, a, b) d = vsubq_f64(c, vmulq_f64(a, b)); -// d = c + a * b[i] -#define vfmla_lane(d, c, a, b, i) d = vaddq_f64(c, vmulq_laneq_f64(a, b, i)); - -#define vfmls_lane(d, c, a, b, i) d = vsubq_f64(c, vmulq_laneq_f64(a, b, i)); - -#endif - -#endif
@@ -1,420 +0,0 @@
-/* - * This file is extracted from the implementation of the FFT on Arm64/Neon - * available in https://github.com/cothan/Falcon-Arm (neon/macrof.h). - * ============================================================================= - * Copyright (c) 2022 by Cryptographic Engineering Research Group (CERG) - * ECE Department, George Mason University - * Fairfax, VA, U.S.A. - * @author: Duc Tri Nguyen dnguye69@gmu.edu, cothannguyen@gmail.com - * Licensed under the Apache License, Version 2.0 (the "License"); - * ============================================================================= - * - * This 64-bit Floating point NEON macro x4 has not been modified and is provided as is. - */ - -#ifndef MACROFX4_H -#define MACROFX4_H - -#include <arm_neon.h> - -#include "macrof.h" - -#define vloadx4(c, addr) c = vld1q_f64_x4(addr); - -#define vstorex4(addr, c) vst1q_f64_x4(addr, c); - -#define vfdupx4(c, constant) \ - c.val[0] = vdupq_n_f64(constant); \ - c.val[1] = vdupq_n_f64(constant); \ - c.val[2] = vdupq_n_f64(constant); \ - c.val[3] = vdupq_n_f64(constant); - -#define vfnegx4(c, a) \ - c.val[0] = vnegq_f64(a.val[0]); \ - c.val[1] = vnegq_f64(a.val[1]); \ - c.val[2] = vnegq_f64(a.val[2]); \ - c.val[3] = vnegq_f64(a.val[3]); - -#define vfmulnx4(c, a, n) \ - c.val[0] = vmulq_n_f64(a.val[0], n); \ - c.val[1] = vmulq_n_f64(a.val[1], n); \ - c.val[2] = vmulq_n_f64(a.val[2], n); \ - c.val[3] = vmulq_n_f64(a.val[3], n); - -// c = a - b -#define vfsubx4(c, a, b) \ - c.val[0] = vsubq_f64(a.val[0], b.val[0]); \ - c.val[1] = vsubq_f64(a.val[1], b.val[1]); \ - c.val[2] = vsubq_f64(a.val[2], b.val[2]); \ - c.val[3] = vsubq_f64(a.val[3], b.val[3]); - -// c = a + b -#define vfaddx4(c, a, b) \ - c.val[0] = vaddq_f64(a.val[0], b.val[0]); \ - c.val[1] = vaddq_f64(a.val[1], b.val[1]); \ - c.val[2] = vaddq_f64(a.val[2], b.val[2]); \ - c.val[3] = vaddq_f64(a.val[3], b.val[3]); - -#define vfmulx4(c, a, b) \ - c.val[0] = vmulq_f64(a.val[0], b.val[0]); \ - c.val[1] = vmulq_f64(a.val[1], b.val[1]); \ - c.val[2] = vmulq_f64(a.val[2], b.val[2]); \ - c.val[3] = vmulq_f64(a.val[3], b.val[3]); - -#define vfmulx4_i(c, a, b) \ - c.val[0] = vmulq_f64(a.val[0], b); \ - c.val[1] = vmulq_f64(a.val[1], b); \ - c.val[2] = vmulq_f64(a.val[2], b); \ - c.val[3] = vmulq_f64(a.val[3], b); - -#define vfinvx4(c, a) \ - c.val[0] = vdivq_f64(vdupq_n_f64(1.0), a.val[0]); \ - c.val[1] = vdivq_f64(vdupq_n_f64(1.0), a.val[1]); \ - c.val[2] = vdivq_f64(vdupq_n_f64(1.0), a.val[2]); \ - c.val[3] = vdivq_f64(vdupq_n_f64(1.0), a.val[3]); - -#define vfcvtx4(c, a) \ - c.val[0] = vcvtq_f64_s64(a.val[0]); \ - c.val[1] = vcvtq_f64_s64(a.val[1]); \ - c.val[2] = vcvtq_f64_s64(a.val[2]); \ - c.val[3] = vcvtq_f64_s64(a.val[3]); - -#define vfmlax4(d, c, a, b) \ - vfmla(d.val[0], c.val[0], a.val[0], b.val[0]); \ - vfmla(d.val[1], c.val[1], a.val[1], b.val[1]); \ - vfmla(d.val[2], c.val[2], a.val[2], b.val[2]); \ - vfmla(d.val[3], c.val[3], a.val[3], b.val[3]); - -#define vfmlsx4(d, c, a, b) \ - vfmls(d.val[0], c.val[0], a.val[0], b.val[0]); \ - vfmls(d.val[1], c.val[1], a.val[1], b.val[1]); \ - vfmls(d.val[2], c.val[2], a.val[2], b.val[2]); \ - vfmls(d.val[3], c.val[3], a.val[3], b.val[3]); - -#define vfrintx4(c, a) \ - c.val[0] = vcvtnq_s64_f64(a.val[0]); \ - c.val[1] = vcvtnq_s64_f64(a.val[1]); \ - c.val[2] = vcvtnq_s64_f64(a.val[2]); \ - c.val[3] = vcvtnq_s64_f64(a.val[3]); - -/* - * Wrapper for FFT, split/merge and poly_float.c - */ - -#define FPC_MUL(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmul(d_re, a_re, b_re); \ - vfmls(d_re, d_re, a_im, b_im); \ - vfmul(d_im, a_re, b_im); \ - vfmla(d_im, d_im, a_im, b_re); - -#define FPC_MULx2(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmul(d_re.val[0], a_re.val[0], b_re.val[0]); \ - vfmls(d_re.val[0], d_re.val[0], a_im.val[0], b_im.val[0]); \ - vfmul(d_re.val[1], a_re.val[1], b_re.val[1]); \ - vfmls(d_re.val[1], d_re.val[1], a_im.val[1], b_im.val[1]); \ - vfmul(d_im.val[0], a_re.val[0], b_im.val[0]); \ - vfmla(d_im.val[0], d_im.val[0], a_im.val[0], b_re.val[0]); \ - vfmul(d_im.val[1], a_re.val[1], b_im.val[1]); \ - vfmla(d_im.val[1], d_im.val[1], a_im.val[1], b_re.val[1]); - -#define FPC_MULx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmul(d_re.val[0], a_re.val[0], b_re.val[0]); \ - vfmls(d_re.val[0], d_re.val[0], a_im.val[0], b_im.val[0]); \ - vfmul(d_re.val[1], a_re.val[1], b_re.val[1]); \ - vfmls(d_re.val[1], d_re.val[1], a_im.val[1], b_im.val[1]); \ - vfmul(d_re.val[2], a_re.val[2], b_re.val[2]); \ - vfmls(d_re.val[2], d_re.val[2], a_im.val[2], b_im.val[2]); \ - vfmul(d_re.val[3], a_re.val[3], b_re.val[3]); \ - vfmls(d_re.val[3], d_re.val[3], a_im.val[3], b_im.val[3]); \ - vfmul(d_im.val[0], a_re.val[0], b_im.val[0]); \ - vfmla(d_im.val[0], d_im.val[0], a_im.val[0], b_re.val[0]); \ - vfmul(d_im.val[1], a_re.val[1], b_im.val[1]); \ - vfmla(d_im.val[1], d_im.val[1], a_im.val[1], b_re.val[1]); \ - vfmul(d_im.val[2], a_re.val[2], b_im.val[2]); \ - vfmla(d_im.val[2], d_im.val[2], a_im.val[2], b_re.val[2]); \ - vfmul(d_im.val[3], a_re.val[3], b_im.val[3]); \ - vfmla(d_im.val[3], d_im.val[3], a_im.val[3], b_re.val[3]); - -#define FPC_MLA(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmla(d_re, d_re, a_re, b_re); \ - vfmls(d_re, d_re, a_im, b_im); \ - vfmla(d_im, d_im, a_re, b_im); \ - vfmla(d_im, d_im, a_im, b_re); - -#define FPC_MLAx2(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmla(d_re.val[0], d_re.val[0], a_re.val[0], b_re.val[0]); \ - vfmls(d_re.val[0], d_re.val[0], a_im.val[0], b_im.val[0]); \ - vfmla(d_re.val[1], d_re.val[1], a_re.val[1], b_re.val[1]); \ - vfmls(d_re.val[1], d_re.val[1], a_im.val[1], b_im.val[1]); \ - vfmla(d_im.val[0], d_im.val[0], a_re.val[0], b_im.val[0]); \ - vfmla(d_im.val[0], d_im.val[0], a_im.val[0], b_re.val[0]); \ - vfmla(d_im.val[1], d_im.val[1], a_re.val[1], b_im.val[1]); \ - vfmla(d_im.val[1], d_im.val[1], a_im.val[1], b_re.val[1]); - -#define FPC_MLAx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmla(d_re.val[0], d_re.val[0], a_re.val[0], b_re.val[0]); \ - vfmls(d_re.val[0], d_re.val[0], a_im.val[0], b_im.val[0]); \ - vfmla(d_re.val[1], d_re.val[1], a_re.val[1], b_re.val[1]); \ - vfmls(d_re.val[1], d_re.val[1], a_im.val[1], b_im.val[1]); \ - vfmla(d_re.val[2], d_re.val[2], a_re.val[2], b_re.val[2]); \ - vfmls(d_re.val[2], d_re.val[2], a_im.val[2], b_im.val[2]); \ - vfmla(d_re.val[3], d_re.val[3], a_re.val[3], b_re.val[3]); \ - vfmls(d_re.val[3], d_re.val[3], a_im.val[3], b_im.val[3]); \ - vfmla(d_im.val[0], d_im.val[0], a_re.val[0], b_im.val[0]); \ - vfmla(d_im.val[0], d_im.val[0], a_im.val[0], b_re.val[0]); \ - vfmla(d_im.val[1], d_im.val[1], a_re.val[1], b_im.val[1]); \ - vfmla(d_im.val[1], d_im.val[1], a_im.val[1], b_re.val[1]); \ - vfmla(d_im.val[2], d_im.val[2], a_re.val[2], b_im.val[2]); \ - vfmla(d_im.val[2], d_im.val[2], a_im.val[2], b_re.val[2]); \ - vfmla(d_im.val[3], d_im.val[3], a_re.val[3], b_im.val[3]); \ - vfmla(d_im.val[3], d_im.val[3], a_im.val[3], b_re.val[3]); - -#define FPC_MUL_CONJx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmul(d_re.val[0], b_im.val[0], a_im.val[0]); \ - vfmla(d_re.val[0], d_re.val[0], a_re.val[0], b_re.val[0]); \ - vfmul(d_re.val[1], b_im.val[1], a_im.val[1]); \ - vfmla(d_re.val[1], d_re.val[1], a_re.val[1], b_re.val[1]); \ - vfmul(d_re.val[2], b_im.val[2], a_im.val[2]); \ - vfmla(d_re.val[2], d_re.val[2], a_re.val[2], b_re.val[2]); \ - vfmul(d_re.val[3], b_im.val[3], a_im.val[3]); \ - vfmla(d_re.val[3], d_re.val[3], a_re.val[3], b_re.val[3]); \ - vfmul(d_im.val[0], b_re.val[0], a_im.val[0]); \ - vfmls(d_im.val[0], d_im.val[0], a_re.val[0], b_im.val[0]); \ - vfmul(d_im.val[1], b_re.val[1], a_im.val[1]); \ - vfmls(d_im.val[1], d_im.val[1], a_re.val[1], b_im.val[1]); \ - vfmul(d_im.val[2], b_re.val[2], a_im.val[2]); \ - vfmls(d_im.val[2], d_im.val[2], a_re.val[2], b_im.val[2]); \ - vfmul(d_im.val[3], b_re.val[3], a_im.val[3]); \ - vfmls(d_im.val[3], d_im.val[3], a_re.val[3], b_im.val[3]); - -#define FPC_MLA_CONJx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmla(d_re.val[0], d_re.val[0], b_im.val[0], a_im.val[0]); \ - vfmla(d_re.val[0], d_re.val[0], a_re.val[0], b_re.val[0]); \ - vfmla(d_re.val[1], d_re.val[1], b_im.val[1], a_im.val[1]); \ - vfmla(d_re.val[1], d_re.val[1], a_re.val[1], b_re.val[1]); \ - vfmla(d_re.val[2], d_re.val[2], b_im.val[2], a_im.val[2]); \ - vfmla(d_re.val[2], d_re.val[2], a_re.val[2], b_re.val[2]); \ - vfmla(d_re.val[3], d_re.val[3], b_im.val[3], a_im.val[3]); \ - vfmla(d_re.val[3], d_re.val[3], a_re.val[3], b_re.val[3]); \ - vfmla(d_im.val[0], d_im.val[0], b_re.val[0], a_im.val[0]); \ - vfmls(d_im.val[0], d_im.val[0], a_re.val[0], b_im.val[0]); \ - vfmla(d_im.val[1], d_im.val[1], b_re.val[1], a_im.val[1]); \ - vfmls(d_im.val[1], d_im.val[1], a_re.val[1], b_im.val[1]); \ - vfmla(d_im.val[2], d_im.val[2], b_re.val[2], a_im.val[2]); \ - vfmls(d_im.val[2], d_im.val[2], a_re.val[2], b_im.val[2]); \ - vfmla(d_im.val[3], d_im.val[3], b_re.val[3], a_im.val[3]); \ - vfmls(d_im.val[3], d_im.val[3], a_re.val[3], b_im.val[3]); - -#define FPC_MUL_LANE(d_re, d_im, a_re, a_im, b_re_im) \ - vfmul_lane(d_re, a_re, b_re_im, 0); \ - vfmls_lane(d_re, d_re, a_im, b_re_im, 1); \ - vfmul_lane(d_im, a_re, b_re_im, 1); \ - vfmla_lane(d_im, d_im, a_im, b_re_im, 0); - -#define FPC_MUL_LANEx4(d_re, d_im, a_re, a_im, b_re_im) \ - vfmul_lane(d_re.val[0], a_re.val[0], b_re_im, 0); \ - vfmls_lane(d_re.val[0], d_re.val[0], a_im.val[0], b_re_im, 1); \ - vfmul_lane(d_re.val[1], a_re.val[1], b_re_im, 0); \ - vfmls_lane(d_re.val[1], d_re.val[1], a_im.val[1], b_re_im, 1); \ - vfmul_lane(d_re.val[2], a_re.val[2], b_re_im, 0); \ - vfmls_lane(d_re.val[2], d_re.val[2], a_im.val[2], b_re_im, 1); \ - vfmul_lane(d_re.val[3], a_re.val[3], b_re_im, 0); \ - vfmls_lane(d_re.val[3], d_re.val[3], a_im.val[3], b_re_im, 1); \ - vfmul_lane(d_im.val[0], a_re.val[0], b_re_im, 1); \ - vfmla_lane(d_im.val[0], d_im.val[0], a_im.val[0], b_re_im, 0); \ - vfmul_lane(d_im.val[1], a_re.val[1], b_re_im, 1); \ - vfmla_lane(d_im.val[1], d_im.val[1], a_im.val[1], b_re_im, 0); \ - vfmul_lane(d_im.val[2], a_re.val[2], b_re_im, 1); \ - vfmla_lane(d_im.val[2], d_im.val[2], a_im.val[2], b_re_im, 0); \ - vfmul_lane(d_im.val[3], a_re.val[3], b_re_im, 1); \ - vfmla_lane(d_im.val[3], d_im.val[3], a_im.val[3], b_re_im, 0); - -#define FWD_TOP(t_re, t_im, b_re, b_im, zeta_re, zeta_im) FPC_MUL(t_re, t_im, b_re, b_im, zeta_re, zeta_im); - -#define FWD_TOP_LANE(t_re, t_im, b_re, b_im, zeta) FPC_MUL_LANE(t_re, t_im, b_re, b_im, zeta); - -#define FWD_TOP_LANEx4(t_re, t_im, b_re, b_im, zeta) FPC_MUL_LANEx4(t_re, t_im, b_re, b_im, zeta); - -/* - * FPC - */ - -#define FPC_SUB(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re = vsubq_f64(a_re, b_re); \ - d_im = vsubq_f64(a_im, b_im); - -#define FPC_SUBx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re.val[0] = vsubq_f64(a_re.val[0], b_re.val[0]); \ - d_im.val[0] = vsubq_f64(a_im.val[0], b_im.val[0]); \ - d_re.val[1] = vsubq_f64(a_re.val[1], b_re.val[1]); \ - d_im.val[1] = vsubq_f64(a_im.val[1], b_im.val[1]); \ - d_re.val[2] = vsubq_f64(a_re.val[2], b_re.val[2]); \ - d_im.val[2] = vsubq_f64(a_im.val[2], b_im.val[2]); \ - d_re.val[3] = vsubq_f64(a_re.val[3], b_re.val[3]); \ - d_im.val[3] = vsubq_f64(a_im.val[3], b_im.val[3]); - -#define FPC_ADD(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re = vaddq_f64(a_re, b_re); \ - d_im = vaddq_f64(a_im, b_im); - -#define FPC_ADDx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re.val[0] = vaddq_f64(a_re.val[0], b_re.val[0]); \ - d_im.val[0] = vaddq_f64(a_im.val[0], b_im.val[0]); \ - d_re.val[1] = vaddq_f64(a_re.val[1], b_re.val[1]); \ - d_im.val[1] = vaddq_f64(a_im.val[1], b_im.val[1]); \ - d_re.val[2] = vaddq_f64(a_re.val[2], b_re.val[2]); \ - d_im.val[2] = vaddq_f64(a_im.val[2], b_im.val[2]); \ - d_re.val[3] = vaddq_f64(a_re.val[3], b_re.val[3]); \ - d_im.val[3] = vaddq_f64(a_im.val[3], b_im.val[3]); - -#define FWD_BOT(a_re, a_im, b_re, b_im, t_re, t_im) \ - FPC_SUB(b_re, b_im, a_re, a_im, t_re, t_im); \ - FPC_ADD(a_re, a_im, a_re, a_im, t_re, t_im); - -#define FWD_BOTx4(a_re, a_im, b_re, b_im, t_re, t_im) \ - FPC_SUBx4(b_re, b_im, a_re, a_im, t_re, t_im); \ - FPC_ADDx4(a_re, a_im, a_re, a_im, t_re, t_im); - -/* - * FPC_J - */ - -#define FPC_ADDJ(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re = vsubq_f64(a_re, b_im); \ - d_im = vaddq_f64(a_im, b_re); - -#define FPC_ADDJx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re.val[0] = vsubq_f64(a_re.val[0], b_im.val[0]); \ - d_im.val[0] = vaddq_f64(a_im.val[0], b_re.val[0]); \ - d_re.val[1] = vsubq_f64(a_re.val[1], b_im.val[1]); \ - d_im.val[1] = vaddq_f64(a_im.val[1], b_re.val[1]); \ - d_re.val[2] = vsubq_f64(a_re.val[2], b_im.val[2]); \ - d_im.val[2] = vaddq_f64(a_im.val[2], b_re.val[2]); \ - d_re.val[3] = vsubq_f64(a_re.val[3], b_im.val[3]); \ - d_im.val[3] = vaddq_f64(a_im.val[3], b_re.val[3]); - -#define FPC_SUBJ(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re = vaddq_f64(a_re, b_im); \ - d_im = vsubq_f64(a_im, b_re); - -#define FPC_SUBJx4(d_re, d_im, a_re, a_im, b_re, b_im) \ - d_re.val[0] = vaddq_f64(a_re.val[0], b_im.val[0]); \ - d_im.val[0] = vsubq_f64(a_im.val[0], b_re.val[0]); \ - d_re.val[1] = vaddq_f64(a_re.val[1], b_im.val[1]); \ - d_im.val[1] = vsubq_f64(a_im.val[1], b_re.val[1]); \ - d_re.val[2] = vaddq_f64(a_re.val[2], b_im.val[2]); \ - d_im.val[2] = vsubq_f64(a_im.val[2], b_re.val[2]); \ - d_re.val[3] = vaddq_f64(a_re.val[3], b_im.val[3]); \ - d_im.val[3] = vsubq_f64(a_im.val[3], b_re.val[3]); - -#define FWD_BOTJ(a_re, a_im, b_re, b_im, t_re, t_im) \ - FPC_SUBJ(b_re, b_im, a_re, a_im, t_re, t_im); \ - FPC_ADDJ(a_re, a_im, a_re, a_im, t_re, t_im); - -#define FWD_BOTJx4(a_re, a_im, b_re, b_im, t_re, t_im) \ - FPC_SUBJx4(b_re, b_im, a_re, a_im, t_re, t_im); \ - FPC_ADDJx4(a_re, a_im, a_re, a_im, t_re, t_im); - -//============== Inverse FFT -/* - * FPC_J - * a * conj(b) - * Original (without swap): - * d_re = b_im * a_im + a_re * b_re; - * d_im = b_re * a_im - a_re * b_im; - */ -#define FPC_MUL_BOTJ_LANE(d_re, d_im, a_re, a_im, b_re_im) \ - vfmul_lane(d_re, a_re, b_re_im, 0); \ - vfmla_lane(d_re, d_re, a_im, b_re_im, 1); \ - vfmul_lane(d_im, a_im, b_re_im, 0); \ - vfmls_lane(d_im, d_im, a_re, b_re_im, 1); - -#define FPC_MUL_BOTJ_LANEx4(d_re, d_im, a_re, a_im, b_re_im) \ - vfmul_lane(d_re.val[0], a_re.val[0], b_re_im, 0); \ - vfmla_lane(d_re.val[0], d_re.val[0], a_im.val[0], b_re_im, 1); \ - vfmul_lane(d_im.val[0], a_im.val[0], b_re_im, 0); \ - vfmls_lane(d_im.val[0], d_im.val[0], a_re.val[0], b_re_im, 1); \ - vfmul_lane(d_re.val[1], a_re.val[1], b_re_im, 0); \ - vfmla_lane(d_re.val[1], d_re.val[1], a_im.val[1], b_re_im, 1); \ - vfmul_lane(d_im.val[1], a_im.val[1], b_re_im, 0); \ - vfmls_lane(d_im.val[1], d_im.val[1], a_re.val[1], b_re_im, 1); \ - vfmul_lane(d_re.val[2], a_re.val[2], b_re_im, 0); \ - vfmla_lane(d_re.val[2], d_re.val[2], a_im.val[2], b_re_im, 1); \ - vfmul_lane(d_im.val[2], a_im.val[2], b_re_im, 0); \ - vfmls_lane(d_im.val[2], d_im.val[2], a_re.val[2], b_re_im, 1); \ - vfmul_lane(d_re.val[3], a_re.val[3], b_re_im, 0); \ - vfmla_lane(d_re.val[3], d_re.val[3], a_im.val[3], b_re_im, 1); \ - vfmul_lane(d_im.val[3], a_im.val[3], b_re_im, 0); \ - vfmls_lane(d_im.val[3], d_im.val[3], a_re.val[3], b_re_im, 1); - -#define FPC_MUL_BOTJ(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmul(d_re, b_im, a_im); \ - vfmla(d_re, d_re, a_re, b_re); \ - vfmul(d_im, b_re, a_im); \ - vfmls(d_im, d_im, a_re, b_im); - -#define INV_TOPJ(t_re, t_im, a_re, a_im, b_re, b_im) \ - FPC_SUB(t_re, t_im, a_re, a_im, b_re, b_im); \ - FPC_ADD(a_re, a_im, a_re, a_im, b_re, b_im); - -#define INV_TOPJx4(t_re, t_im, a_re, a_im, b_re, b_im) \ - FPC_SUBx4(t_re, t_im, a_re, a_im, b_re, b_im); \ - FPC_ADDx4(a_re, a_im, a_re, a_im, b_re, b_im); - -#define INV_BOTJ(b_re, b_im, t_re, t_im, zeta_re, zeta_im) FPC_MUL_BOTJ(b_re, b_im, t_re, t_im, zeta_re, zeta_im); - -#define INV_BOTJ_LANE(b_re, b_im, t_re, t_im, zeta) FPC_MUL_BOTJ_LANE(b_re, b_im, t_re, t_im, zeta); - -#define INV_BOTJ_LANEx4(b_re, b_im, t_re, t_im, zeta) FPC_MUL_BOTJ_LANEx4(b_re, b_im, t_re, t_im, zeta); - -/* - * FPC_Jm - * a * -conj(b) - * d_re = a_re * b_im - a_im * b_re; - * d_im = a_im * b_im + a_re * b_re; - */ -#define FPC_MUL_BOTJm_LANE(d_re, d_im, a_re, a_im, b_re_im) \ - vfmul_lane(d_re, a_re, b_re_im, 1); \ - vfmls_lane(d_re, d_re, a_im, b_re_im, 0); \ - vfmul_lane(d_im, a_re, b_re_im, 0); \ - vfmla_lane(d_im, d_im, a_im, b_re_im, 1); - -#define FPC_MUL_BOTJm_LANEx4(d_re, d_im, a_re, a_im, b_re_im) \ - vfmul_lane(d_re.val[0], a_re.val[0], b_re_im, 1); \ - vfmls_lane(d_re.val[0], d_re.val[0], a_im.val[0], b_re_im, 0); \ - vfmul_lane(d_im.val[0], a_re.val[0], b_re_im, 0); \ - vfmla_lane(d_im.val[0], d_im.val[0], a_im.val[0], b_re_im, 1); \ - vfmul_lane(d_re.val[1], a_re.val[1], b_re_im, 1); \ - vfmls_lane(d_re.val[1], d_re.val[1], a_im.val[1], b_re_im, 0); \ - vfmul_lane(d_im.val[1], a_re.val[1], b_re_im, 0); \ - vfmla_lane(d_im.val[1], d_im.val[1], a_im.val[1], b_re_im, 1); \ - vfmul_lane(d_re.val[2], a_re.val[2], b_re_im, 1); \ - vfmls_lane(d_re.val[2], d_re.val[2], a_im.val[2], b_re_im, 0); \ - vfmul_lane(d_im.val[2], a_re.val[2], b_re_im, 0); \ - vfmla_lane(d_im.val[2], d_im.val[2], a_im.val[2], b_re_im, 1); \ - vfmul_lane(d_re.val[3], a_re.val[3], b_re_im, 1); \ - vfmls_lane(d_re.val[3], d_re.val[3], a_im.val[3], b_re_im, 0); \ - vfmul_lane(d_im.val[3], a_re.val[3], b_re_im, 0); \ - vfmla_lane(d_im.val[3], d_im.val[3], a_im.val[3], b_re_im, 1); - -#define FPC_MUL_BOTJm(d_re, d_im, a_re, a_im, b_re, b_im) \ - vfmul(d_re, a_re, b_im); \ - vfmls(d_re, d_re, a_im, b_re); \ - vfmul(d_im, a_im, b_im); \ - vfmla(d_im, d_im, a_re, b_re); - -#define INV_TOPJm(t_re, t_im, a_re, a_im, b_re, b_im) \ - FPC_SUB(t_re, t_im, b_re, b_im, a_re, a_im); \ - FPC_ADD(a_re, a_im, a_re, a_im, b_re, b_im); - -#define INV_TOPJmx4(t_re, t_im, a_re, a_im, b_re, b_im) \ - FPC_SUBx4(t_re, t_im, b_re, b_im, a_re, a_im); \ - FPC_ADDx4(a_re, a_im, a_re, a_im, b_re, b_im); - -#define INV_BOTJm(b_re, b_im, t_re, t_im, zeta_re, zeta_im) FPC_MUL_BOTJm(b_re, b_im, t_re, t_im, zeta_re, zeta_im); - -#define INV_BOTJm_LANE(b_re, b_im, t_re, t_im, zeta) FPC_MUL_BOTJm_LANE(b_re, b_im, t_re, t_im, zeta); - -#define INV_BOTJm_LANEx4(b_re, b_im, t_re, t_im, zeta) FPC_MUL_BOTJm_LANEx4(b_re, b_im, t_re, t_im, zeta); - -#endif
@@ -1,115 +0,0 @@
-#ifndef SPQLIOS_Q120_ARITHMETIC_H -#define SPQLIOS_Q120_ARITHMETIC_H - -#include <stdint.h> - -#include "../commons.h" -#include "q120_common.h" - -typedef struct _q120_mat1col_product_baa_precomp q120_mat1col_product_baa_precomp; -typedef struct _q120_mat1col_product_bbb_precomp q120_mat1col_product_bbb_precomp; -typedef struct _q120_mat1col_product_bbc_precomp q120_mat1col_product_bbc_precomp; - -EXPORT q120_mat1col_product_baa_precomp* q120_new_vec_mat1col_product_baa_precomp(); -EXPORT void q120_delete_vec_mat1col_product_baa_precomp(q120_mat1col_product_baa_precomp*); -EXPORT q120_mat1col_product_bbb_precomp* q120_new_vec_mat1col_product_bbb_precomp(); -EXPORT void q120_delete_vec_mat1col_product_bbb_precomp(q120_mat1col_product_bbb_precomp*); -EXPORT q120_mat1col_product_bbc_precomp* q120_new_vec_mat1col_product_bbc_precomp(); -EXPORT void q120_delete_vec_mat1col_product_bbc_precomp(q120_mat1col_product_bbc_precomp*); - -// ell < 10000 -EXPORT void q120_vec_mat1col_product_baa_ref(q120_mat1col_product_baa_precomp*, const uint64_t ell, q120b* const res, - const q120a* const x, const q120a* const y); -EXPORT void q120_vec_mat1col_product_bbb_ref(q120_mat1col_product_bbb_precomp*, const uint64_t ell, q120b* const res, - const q120b* const x, const q120b* const y); -EXPORT void q120_vec_mat1col_product_bbc_ref(q120_mat1col_product_bbc_precomp*, const uint64_t ell, q120b* const res, - const q120b* const x, const q120c* const y); - -EXPORT void q120_vec_mat1col_product_baa_avx2(q120_mat1col_product_baa_precomp*, const uint64_t ell, q120b* const res, - const q120a* const x, const q120a* const y); -EXPORT void q120_vec_mat1col_product_bbb_avx2(q120_mat1col_product_bbb_precomp*, const uint64_t ell, q120b* const res, - const q120b* const x, const q120b* const y); -EXPORT void q120_vec_mat1col_product_bbc_avx2(q120_mat1col_product_bbc_precomp*, const uint64_t ell, q120b* const res, - const q120b* const x, const q120c* const y); - -EXPORT void q120x2_vec_mat1col_product_bbc_ref(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); -EXPORT void q120x2_vec_mat1col_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); -EXPORT void q120x2_vec_mat2cols_product_bbc_ref(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); -EXPORT void q120x2_vec_mat2cols_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); - -/** - * @brief extract 1 q120x2 block from one q120 ntt vectors - * @param nn the size of each vector - * @param blk the block id to extract (<nn/2) - * @param dst the output: nrows q120x2's dst[i] = src[i](blk) - * @param src the input: nrows q120 ntt vecs's - */ -EXPORT void q120x2_extract_1blk_from_q120b_ref(uint64_t nn, uint64_t blk, - q120x2b* const dst, // 8 doubles - const q120b* const src // a reim vector -); -EXPORT void q120x2_extract_1blk_from_q120c_ref(uint64_t nn, uint64_t blk, - q120x2c* const dst, // 8 doubles - const q120c* const src // a reim vector -); -EXPORT void q120x2_extract_1blk_from_q120b_avx(uint64_t nn, uint64_t blk, - q120x2b* const dst, // 8 doubles - const q120b* const src // a reim vector -); -EXPORT void q120x2_extract_1blk_from_q120c_avx(uint64_t nn, uint64_t blk, - q120x2c* const dst, // 8 doubles - const q120c* const src // a reim vector -); - -/** - * @brief extract 1 reim4 block from nrows reim vectors of m complexes - * @param nn the size of each q120 - * @param nrows the number of q120 (ntt) vectors - * @param blk the block id to extract (<m/4) - * @param dst the output: nrows q120x2's dst[i] = src[i](blk) - * @param src the input: nrows q120 ntt vectors - */ -EXPORT void q120x2_extract_1blk_from_contiguous_q120b_ref( - uint64_t nn, uint64_t nrows, uint64_t blk, - q120x2b* const dst, // nrows * 2 q120 - const q120b* const src // a contiguous array of nrows q120b vectors -); -EXPORT void q120x2_extract_1blk_from_contiguous_q120b_avx( - uint64_t nn, uint64_t nrows, uint64_t blk, - q120x2b* const dst, // nrows * 2 q120 - const q120b* const src // a contiguous array of nrows q120b vectors -); - -/** - * @brief saves 1 single q120x2 block in a q120 vectors of size nn - * @param nn the size of the output q120 - * @param blk the block id to save (<nn/2) - * @param dest the output q120b vector: dst(blk) = src - * @param src the input q120x2b - */ -EXPORT void q120x2b_save_1blk_to_q120b_ref(uint64_t nn, uint64_t blk, - q120b* dest, // 1 reim vector of length m - const q120x2b* src // 8 doubles -); -EXPORT void q120x2b_save_1blk_to_q120b_avx(uint64_t nn, uint64_t blk, - q120b* dest, // 1 reim vector of length m - const q120x2b* src // 8 doubles -); - -EXPORT void q120_add_bbb_simple(uint64_t nn, q120b* const res, const q120b* const x, const q120b* const y); - -EXPORT void q120_add_ccc_simple(uint64_t nn, q120c* const res, const q120c* const x, const q120c* const y); - -EXPORT void q120_c_from_b_simple(uint64_t nn, q120c* const res, const q120b* const x); - -EXPORT void q120_b_from_znx64_simple(uint64_t nn, q120b* const res, const int64_t* const x); - -EXPORT void q120_c_from_znx64_simple(uint64_t nn, q120c* const res, const int64_t* const x); - -EXPORT void q120_b_to_znx128_simple(uint64_t nn, __int128_t* const res, const q120b* const x); - -#endif // SPQLIOS_Q120_ARITHMETIC_H
@@ -1,567 +0,0 @@
-#include <immintrin.h> - -#include "q120_arithmetic.h" -#include "q120_arithmetic_private.h" - -EXPORT void q120_vec_mat1col_product_baa_avx2(q120_mat1col_product_baa_precomp* precomp, const uint64_t ell, - q120b* const res, const q120a* const x, const q120a* const y) { - /** - * Algorithm: - * - res = acc1 + acc2 . ((2^H) % Q) - * - acc1 is the sum of H LSB of products x[i].y[i] - * - acc2 is the sum of 64-H MSB of products x[i]].y[i] - * - for l < 10k acc1 will have H + log2(10000) and acc2 64 - H + log2(10000) bits - * - final sum has max(H, 64 - H + bit_size((2^H) % Q)) + log2(10000) + 1 bits - */ - - const uint64_t H = precomp->h; - const __m256i MASK = _mm256_set1_epi64x((UINT64_C(1) << H) - 1); - - __m256i acc1 = _mm256_setzero_si256(); - __m256i acc2 = _mm256_setzero_si256(); - - const __m256i* x_ptr = (__m256i*)x; - const __m256i* y_ptr = (__m256i*)y; - - for (uint64_t i = 0; i < ell; ++i) { - __m256i a = _mm256_loadu_si256(x_ptr); - __m256i b = _mm256_loadu_si256(y_ptr); - __m256i t = _mm256_mul_epu32(a, b); - - acc1 = _mm256_add_epi64(acc1, _mm256_and_si256(t, MASK)); - acc2 = _mm256_add_epi64(acc2, _mm256_srli_epi64(t, H)); - - x_ptr++; - y_ptr++; - } - - const __m256i H_POW_RED = _mm256_loadu_si256((__m256i*)precomp->h_pow_red); - - __m256i t = _mm256_add_epi64(acc1, _mm256_mul_epu32(acc2, H_POW_RED)); - _mm256_storeu_si256((__m256i*)res, t); -} - -EXPORT void q120_vec_mat1col_product_bbb_avx2(q120_mat1col_product_bbb_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120b* const y) { - /** - * Algorithm: - * 1. Split x_i and y_i in 2 32-bit parts and compute the cross-products: - * - x_i = xl_i + xh_i . 2^32 - * - y_i = yl_i + yh_i . 2^32 - * - A_i = xl_i . yl_i - * - B_i = xl_i . yh_i - * - C_i = xh_i . yl_i - * - D_i = xh_i . yh_i - * - we have x_i . y_i == A_i + (B_i + C_i) . 2^32 + D_i . 2^64 - * 2. Split A_i, B_i, C_i and D_i into 2 32-bit parts - * - A_i = Al_i + Ah_i . 2^32 - * - B_i = Bl_i + Bh_i . 2^32 - * - C_i = Cl_i + Ch_i . 2^32 - * - D_i = Dl_i + Dh_i . 2^32 - * 3. Compute the sums: - * - S1 = \sum Al_i - * - S2 = \sum (Ah_i + Bl_i + Cl_i) - * - S3 = \sum (Bh_i + Ch_i + Dl_i) - * - S4 = \sum Dh_i - * - here S1, S4 have 32 + log2(ell) bits and S2, S3 have 32 + log2(ell) + - * log2(3) bits - * - for ell == 10000 S2, S3 have < 47 bits - * 4. Split S1, S2, S3 and S4 in 2 24-bit parts (24 = ceil(47/2)) - * - S1 = S1l + S1h . 2^24 - * - S2 = S2l + S2h . 2^24 - * - S3 = S3l + S3h . 2^24 - * - S4 = S4l + S4h . 2^24 - * 5. Compute final result as: - * - \sum x_i . y_i = S1l + S1h . 2^24 - * + S2l . 2^32 + S2h . 2^(32+24) - * + S3l . 2^64 + S3h . 2^(64 + 24) - * + S4l . 2^96 + S4l . 2^(96+24) - * - here the powers of 2 are reduced modulo the primes Q before - * multiplications - * - the result will be on 24 + 3 + bit size of primes Q - */ - const uint64_t H1 = 32; - const __m256i MASK1 = _mm256_set1_epi64x((UINT64_C(1) << H1) - 1); - - __m256i s1 = _mm256_setzero_si256(); - __m256i s2 = _mm256_setzero_si256(); - __m256i s3 = _mm256_setzero_si256(); - __m256i s4 = _mm256_setzero_si256(); - - const __m256i* x_ptr = (__m256i*)x; - const __m256i* y_ptr = (__m256i*)y; - - for (uint64_t i = 0; i < ell; ++i) { - __m256i x = _mm256_loadu_si256(x_ptr); - __m256i xl = _mm256_and_si256(x, MASK1); - __m256i xh = _mm256_srli_epi64(x, H1); - - __m256i y = _mm256_loadu_si256(y_ptr); - __m256i yl = _mm256_and_si256(y, MASK1); - __m256i yh = _mm256_srli_epi64(y, H1); - - __m256i a = _mm256_mul_epu32(xl, yl); - __m256i b = _mm256_mul_epu32(xl, yh); - __m256i c = _mm256_mul_epu32(xh, yl); - __m256i d = _mm256_mul_epu32(xh, yh); - - s1 = _mm256_add_epi64(s1, _mm256_and_si256(a, MASK1)); - - s2 = _mm256_add_epi64(s2, _mm256_srli_epi64(a, H1)); - s2 = _mm256_add_epi64(s2, _mm256_and_si256(b, MASK1)); - s2 = _mm256_add_epi64(s2, _mm256_and_si256(c, MASK1)); - - s3 = _mm256_add_epi64(s3, _mm256_srli_epi64(b, H1)); - s3 = _mm256_add_epi64(s3, _mm256_srli_epi64(c, H1)); - s3 = _mm256_add_epi64(s3, _mm256_and_si256(d, MASK1)); - - s4 = _mm256_add_epi64(s4, _mm256_srli_epi64(d, H1)); - - x_ptr++; - y_ptr++; - } - - const uint64_t H2 = precomp->h; - const __m256i MASK2 = _mm256_set1_epi64x((UINT64_C(1) << H2) - 1); - - const __m256i S1H_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s1h_pow_red); - __m256i s1l = _mm256_and_si256(s1, MASK2); - __m256i s1h = _mm256_srli_epi64(s1, H2); - __m256i t = _mm256_add_epi64(s1l, _mm256_mul_epu32(s1h, S1H_POW_RED)); - - const __m256i S2L_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s2l_pow_red); - const __m256i S2H_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s2h_pow_red); - __m256i s2l = _mm256_and_si256(s2, MASK2); - __m256i s2h = _mm256_srli_epi64(s2, H2); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s2l, S2L_POW_RED)); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s2h, S2H_POW_RED)); - - const __m256i S3L_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s3l_pow_red); - const __m256i S3H_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s3h_pow_red); - __m256i s3l = _mm256_and_si256(s3, MASK2); - __m256i s3h = _mm256_srli_epi64(s3, H2); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s3l, S3L_POW_RED)); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s3h, S3H_POW_RED)); - - const __m256i S4L_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s4l_pow_red); - const __m256i S4H_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s4h_pow_red); - __m256i s4l = _mm256_and_si256(s4, MASK2); - __m256i s4h = _mm256_srli_epi64(s4, H2); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s4l, S4L_POW_RED)); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s4h, S4H_POW_RED)); - - _mm256_storeu_si256((__m256i*)res, t); -} - -EXPORT void q120_vec_mat1col_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - /** - * Algorithm: - * 0. We have - * - y0_i == y_i % Q and y1_i == (y_i . 2^32) % Q - * 1. Split x_i in 2 32-bit parts and compute the cross-products: - * - x_i = xl_i + xh_i . 2^32 - * - A_i = xl_i . y1_i - * - B_i = xh_i . y2_i - * - we have x_i . y_i == A_i + B_i - * 2. Split A_i and B_i into 2 32-bit parts - * - A_i = Al_i + Ah_i . 2^32 - * - B_i = Bl_i + Bh_i . 2^32 - * 3. Compute the sums: - * - S1 = \sum Al_i + Bl_i - * - S2 = \sum Ah_i + Bh_i - * - here S1 and S2 have 32 + log2(ell) bits - * - for ell == 10000 S1, S2 have < 46 bits - * 4. Split S2 in 27-bit and 19-bit parts (27+19 == 46) - * - S2 = S2l + S2h . 2^27 - * 5. Compute final result as: - * - \sum x_i . y_i = S1 + S2l . 2^32 + S2h . 2^(32+27) - * - here the powers of 2 are reduced modulo the primes Q before - * multiplications - * - the result will be on < 52 bits - */ - - const uint64_t H1 = 32; - const __m256i MASK1 = _mm256_set1_epi64x((UINT64_C(1) << H1) - 1); - - __m256i s1 = _mm256_setzero_si256(); - __m256i s2 = _mm256_setzero_si256(); - - const __m256i* x_ptr = (__m256i*)x; - const __m256i* y_ptr = (__m256i*)y; - - for (uint64_t i = 0; i < ell; ++i) { - __m256i x = _mm256_loadu_si256(x_ptr); - __m256i xl = _mm256_and_si256(x, MASK1); - __m256i xh = _mm256_srli_epi64(x, H1); - - __m256i y = _mm256_loadu_si256(y_ptr); - __m256i y0 = _mm256_and_si256(y, MASK1); - __m256i y1 = _mm256_srli_epi64(y, H1); - - __m256i a = _mm256_mul_epu32(xl, y0); - __m256i b = _mm256_mul_epu32(xh, y1); - - s1 = _mm256_add_epi64(s1, _mm256_and_si256(a, MASK1)); - s1 = _mm256_add_epi64(s1, _mm256_and_si256(b, MASK1)); - - s2 = _mm256_add_epi64(s2, _mm256_srli_epi64(a, H1)); - s2 = _mm256_add_epi64(s2, _mm256_srli_epi64(b, H1)); - - x_ptr++; - y_ptr++; - } - - const uint64_t H2 = precomp->h; - const __m256i MASK2 = _mm256_set1_epi64x((UINT64_C(1) << H2) - 1); - - __m256i t = s1; - - const __m256i S2L_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s2l_pow_red); - const __m256i S2H_POW_RED = _mm256_loadu_si256((__m256i*)precomp->s2h_pow_red); - __m256i s2l = _mm256_and_si256(s2, MASK2); - __m256i s2h = _mm256_srli_epi64(s2, H2); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s2l, S2L_POW_RED)); - t = _mm256_add_epi64(t, _mm256_mul_epu32(s2h, S2H_POW_RED)); - - _mm256_storeu_si256((__m256i*)res, t); -} - -/** - * @deprecated keeping this one for history only. - * There is a slight register starvation condition on the q120x2_vec_mat2cols - * strategy below sounds better. - */ -EXPORT void q120x2_vec_mat2cols_product_bbc_avx2_old(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - __m256i s0 = _mm256_setzero_si256(); // col 1a - __m256i s1 = _mm256_setzero_si256(); - __m256i s2 = _mm256_setzero_si256(); // col 1b - __m256i s3 = _mm256_setzero_si256(); - __m256i s4 = _mm256_setzero_si256(); // col 2a - __m256i s5 = _mm256_setzero_si256(); - __m256i s6 = _mm256_setzero_si256(); // col 2b - __m256i s7 = _mm256_setzero_si256(); - __m256i s8, s9, s10, s11; - __m256i s12, s13, s14, s15; - - const __m256i* x_ptr = (__m256i*)x; - const __m256i* y_ptr = (__m256i*)y; - __m256i* res_ptr = (__m256i*)res; - for (uint64_t i = 0; i < ell; ++i) { - s8 = _mm256_loadu_si256(x_ptr); - s9 = _mm256_loadu_si256(x_ptr + 1); - s10 = _mm256_srli_epi64(s8, 32); - s11 = _mm256_srli_epi64(s9, 32); - - s12 = _mm256_loadu_si256(y_ptr); - s13 = _mm256_loadu_si256(y_ptr + 1); - s14 = _mm256_srli_epi64(s12, 32); - s15 = _mm256_srli_epi64(s13, 32); - - s12 = _mm256_mul_epu32(s8, s12); // -> s0,s1 - s13 = _mm256_mul_epu32(s9, s13); // -> s2,s3 - s14 = _mm256_mul_epu32(s10, s14); // -> s0,s1 - s15 = _mm256_mul_epu32(s11, s15); // -> s2,s3 - - s10 = _mm256_slli_epi64(s12, 32); // -> s0 - s11 = _mm256_slli_epi64(s13, 32); // -> s2 - s12 = _mm256_srli_epi64(s12, 32); // -> s1 - s13 = _mm256_srli_epi64(s13, 32); // -> s3 - s10 = _mm256_srli_epi64(s10, 32); // -> s0 - s11 = _mm256_srli_epi64(s11, 32); // -> s2 - - s0 = _mm256_add_epi64(s0, s10); - s1 = _mm256_add_epi64(s1, s12); - s2 = _mm256_add_epi64(s2, s11); - s3 = _mm256_add_epi64(s3, s13); - - s10 = _mm256_slli_epi64(s14, 32); // -> s0 - s11 = _mm256_slli_epi64(s15, 32); // -> s2 - s14 = _mm256_srli_epi64(s14, 32); // -> s1 - s15 = _mm256_srli_epi64(s15, 32); // -> s3 - s10 = _mm256_srli_epi64(s10, 32); // -> s0 - s11 = _mm256_srli_epi64(s11, 32); // -> s2 - - s0 = _mm256_add_epi64(s0, s10); - s1 = _mm256_add_epi64(s1, s14); - s2 = _mm256_add_epi64(s2, s11); - s3 = _mm256_add_epi64(s3, s15); - - // deal with the second column - // s8,s9 are still in place! - s10 = _mm256_srli_epi64(s8, 32); - s11 = _mm256_srli_epi64(s9, 32); - - s12 = _mm256_loadu_si256(y_ptr + 2); - s13 = _mm256_loadu_si256(y_ptr + 3); - s14 = _mm256_srli_epi64(s12, 32); - s15 = _mm256_srli_epi64(s13, 32); - - s12 = _mm256_mul_epu32(s8, s12); // -> s4,s5 - s13 = _mm256_mul_epu32(s9, s13); // -> s6,s7 - s14 = _mm256_mul_epu32(s10, s14); // -> s4,s5 - s15 = _mm256_mul_epu32(s11, s15); // -> s6,s7 - - s10 = _mm256_slli_epi64(s12, 32); // -> s4 - s11 = _mm256_slli_epi64(s13, 32); // -> s6 - s12 = _mm256_srli_epi64(s12, 32); // -> s5 - s13 = _mm256_srli_epi64(s13, 32); // -> s7 - s10 = _mm256_srli_epi64(s10, 32); // -> s4 - s11 = _mm256_srli_epi64(s11, 32); // -> s6 - - s4 = _mm256_add_epi64(s4, s10); - s5 = _mm256_add_epi64(s5, s12); - s6 = _mm256_add_epi64(s6, s11); - s7 = _mm256_add_epi64(s7, s13); - - s10 = _mm256_slli_epi64(s14, 32); // -> s4 - s11 = _mm256_slli_epi64(s15, 32); // -> s6 - s14 = _mm256_srli_epi64(s14, 32); // -> s5 - s15 = _mm256_srli_epi64(s15, 32); // -> s7 - s10 = _mm256_srli_epi64(s10, 32); // -> s4 - s11 = _mm256_srli_epi64(s11, 32); // -> s6 - - s4 = _mm256_add_epi64(s4, s10); - s5 = _mm256_add_epi64(s5, s14); - s6 = _mm256_add_epi64(s6, s11); - s7 = _mm256_add_epi64(s7, s15); - - x_ptr += 2; - y_ptr += 4; - } - // final reduction - const uint64_t H2 = precomp->h; - s8 = _mm256_set1_epi64x((UINT64_C(1) << H2) - 1); // MASK2 - s9 = _mm256_loadu_si256((__m256i*)precomp->s2l_pow_red); // S2L_POW_RED - s10 = _mm256_loadu_si256((__m256i*)precomp->s2h_pow_red); // S2H_POW_RED - //--- s0,s1 - s11 = _mm256_and_si256(s1, s8); - s12 = _mm256_srli_epi64(s1, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s0 = _mm256_add_epi64(s0, s13); - s0 = _mm256_add_epi64(s0, s14); - _mm256_storeu_si256(res_ptr + 0, s0); - //--- s2,s3 - s11 = _mm256_and_si256(s3, s8); - s12 = _mm256_srli_epi64(s3, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s2 = _mm256_add_epi64(s2, s13); - s2 = _mm256_add_epi64(s2, s14); - _mm256_storeu_si256(res_ptr + 1, s2); - //--- s4,s5 - s11 = _mm256_and_si256(s5, s8); - s12 = _mm256_srli_epi64(s5, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s4 = _mm256_add_epi64(s4, s13); - s4 = _mm256_add_epi64(s4, s14); - _mm256_storeu_si256(res_ptr + 2, s4); - //--- s6,s7 - s11 = _mm256_and_si256(s7, s8); - s12 = _mm256_srli_epi64(s7, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s6 = _mm256_add_epi64(s6, s13); - s6 = _mm256_add_epi64(s6, s14); - _mm256_storeu_si256(res_ptr + 3, s6); -} - -EXPORT void q120x2_vec_mat2cols_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - __m256i s0 = _mm256_setzero_si256(); // col 1a - __m256i s1 = _mm256_setzero_si256(); - __m256i s2 = _mm256_setzero_si256(); // col 1b - __m256i s3 = _mm256_setzero_si256(); - __m256i s4 = _mm256_setzero_si256(); // col 2a - __m256i s5 = _mm256_setzero_si256(); - __m256i s6 = _mm256_setzero_si256(); // col 2b - __m256i s7 = _mm256_setzero_si256(); - __m256i s8, s9, s10, s11; - __m256i s12, s13, s14, s15; - - s11 = _mm256_set1_epi64x(0xFFFFFFFFUL); - const __m256i* x_ptr = (__m256i*)x; - const __m256i* y_ptr = (__m256i*)y; - __m256i* res_ptr = (__m256i*)res; - for (uint64_t i = 0; i < ell; ++i) { - // treat item a - s8 = _mm256_loadu_si256(x_ptr); - s9 = _mm256_srli_epi64(s8, 32); - - s12 = _mm256_loadu_si256(y_ptr); - s13 = _mm256_loadu_si256(y_ptr + 2); - s14 = _mm256_srli_epi64(s12, 32); - s15 = _mm256_srli_epi64(s13, 32); - - s12 = _mm256_mul_epu32(s8, s12); // c1a -> s0,s1 - s13 = _mm256_mul_epu32(s8, s13); // c2a -> s4,s5 - s14 = _mm256_mul_epu32(s9, s14); // c1a -> s0,s1 - s15 = _mm256_mul_epu32(s9, s15); // c2a -> s4,s5 - - s8 = _mm256_and_si256(s12, s11); // -> s0 - s9 = _mm256_and_si256(s13, s11); // -> s4 - s12 = _mm256_srli_epi64(s12, 32); // -> s1 - s13 = _mm256_srli_epi64(s13, 32); // -> s5 - s0 = _mm256_add_epi64(s0, s8); - s1 = _mm256_add_epi64(s1, s12); - s4 = _mm256_add_epi64(s4, s9); - s5 = _mm256_add_epi64(s5, s13); - - s8 = _mm256_and_si256(s14, s11); // -> s0 - s9 = _mm256_and_si256(s15, s11); // -> s4 - s14 = _mm256_srli_epi64(s14, 32); // -> s1 - s15 = _mm256_srli_epi64(s15, 32); // -> s5 - s0 = _mm256_add_epi64(s0, s8); - s1 = _mm256_add_epi64(s1, s14); - s4 = _mm256_add_epi64(s4, s9); - s5 = _mm256_add_epi64(s5, s15); - - // treat item b - s8 = _mm256_loadu_si256(x_ptr + 1); - s9 = _mm256_srli_epi64(s8, 32); - - s12 = _mm256_loadu_si256(y_ptr + 1); - s13 = _mm256_loadu_si256(y_ptr + 3); - s14 = _mm256_srli_epi64(s12, 32); - s15 = _mm256_srli_epi64(s13, 32); - - s12 = _mm256_mul_epu32(s8, s12); // c1b -> s2,s3 - s13 = _mm256_mul_epu32(s8, s13); // c2b -> s6,s7 - s14 = _mm256_mul_epu32(s9, s14); // c1b -> s2,s3 - s15 = _mm256_mul_epu32(s9, s15); // c2b -> s6,s7 - - s8 = _mm256_and_si256(s12, s11); // -> s2 - s9 = _mm256_and_si256(s13, s11); // -> s6 - s12 = _mm256_srli_epi64(s12, 32); // -> s3 - s13 = _mm256_srli_epi64(s13, 32); // -> s7 - s2 = _mm256_add_epi64(s2, s8); - s3 = _mm256_add_epi64(s3, s12); - s6 = _mm256_add_epi64(s6, s9); - s7 = _mm256_add_epi64(s7, s13); - - s8 = _mm256_and_si256(s14, s11); // -> s2 - s9 = _mm256_and_si256(s15, s11); // -> s6 - s14 = _mm256_srli_epi64(s14, 32); // -> s3 - s15 = _mm256_srli_epi64(s15, 32); // -> s7 - s2 = _mm256_add_epi64(s2, s8); - s3 = _mm256_add_epi64(s3, s14); - s6 = _mm256_add_epi64(s6, s9); - s7 = _mm256_add_epi64(s7, s15); - - x_ptr += 2; - y_ptr += 4; - } - // final reduction - const uint64_t H2 = precomp->h; - s8 = _mm256_set1_epi64x((UINT64_C(1) << H2) - 1); // MASK2 - s9 = _mm256_loadu_si256((__m256i*)precomp->s2l_pow_red); // S2L_POW_RED - s10 = _mm256_loadu_si256((__m256i*)precomp->s2h_pow_red); // S2H_POW_RED - //--- s0,s1 - s11 = _mm256_and_si256(s1, s8); - s12 = _mm256_srli_epi64(s1, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s0 = _mm256_add_epi64(s0, s13); - s0 = _mm256_add_epi64(s0, s14); - _mm256_storeu_si256(res_ptr + 0, s0); - //--- s2,s3 - s11 = _mm256_and_si256(s3, s8); - s12 = _mm256_srli_epi64(s3, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s2 = _mm256_add_epi64(s2, s13); - s2 = _mm256_add_epi64(s2, s14); - _mm256_storeu_si256(res_ptr + 1, s2); - //--- s4,s5 - s11 = _mm256_and_si256(s5, s8); - s12 = _mm256_srli_epi64(s5, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s4 = _mm256_add_epi64(s4, s13); - s4 = _mm256_add_epi64(s4, s14); - _mm256_storeu_si256(res_ptr + 2, s4); - //--- s6,s7 - s11 = _mm256_and_si256(s7, s8); - s12 = _mm256_srli_epi64(s7, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s6 = _mm256_add_epi64(s6, s13); - s6 = _mm256_add_epi64(s6, s14); - _mm256_storeu_si256(res_ptr + 3, s6); -} - -EXPORT void q120x2_vec_mat1col_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - __m256i s0 = _mm256_setzero_si256(); // col 1a - __m256i s1 = _mm256_setzero_si256(); - __m256i s2 = _mm256_setzero_si256(); // col 1b - __m256i s3 = _mm256_setzero_si256(); - __m256i s4 = _mm256_set1_epi64x(0xFFFFFFFFUL); - __m256i s8, s9, s10, s11; - __m256i s12, s13, s14, s15; - - const __m256i* x_ptr = (__m256i*)x; - const __m256i* y_ptr = (__m256i*)y; - __m256i* res_ptr = (__m256i*)res; - for (uint64_t i = 0; i < ell; ++i) { - s8 = _mm256_loadu_si256(x_ptr); - s9 = _mm256_loadu_si256(x_ptr + 1); - s10 = _mm256_srli_epi64(s8, 32); - s11 = _mm256_srli_epi64(s9, 32); - - s12 = _mm256_loadu_si256(y_ptr); - s13 = _mm256_loadu_si256(y_ptr + 1); - s14 = _mm256_srli_epi64(s12, 32); - s15 = _mm256_srli_epi64(s13, 32); - - s12 = _mm256_mul_epu32(s8, s12); // -> s0,s1 - s13 = _mm256_mul_epu32(s9, s13); // -> s2,s3 - s14 = _mm256_mul_epu32(s10, s14); // -> s0,s1 - s15 = _mm256_mul_epu32(s11, s15); // -> s2,s3 - - s8 = _mm256_and_si256(s12, s4); // -> s0 - s9 = _mm256_and_si256(s13, s4); // -> s2 - s10 = _mm256_and_si256(s14, s4); // -> s0 - s11 = _mm256_and_si256(s15, s4); // -> s2 - s12 = _mm256_srli_epi64(s12, 32); // -> s1 - s13 = _mm256_srli_epi64(s13, 32); // -> s3 - s14 = _mm256_srli_epi64(s14, 32); // -> s1 - s15 = _mm256_srli_epi64(s15, 32); // -> s3 - - s0 = _mm256_add_epi64(s0, s8); - s1 = _mm256_add_epi64(s1, s12); - s2 = _mm256_add_epi64(s2, s9); - s3 = _mm256_add_epi64(s3, s13); - s0 = _mm256_add_epi64(s0, s10); - s1 = _mm256_add_epi64(s1, s14); - s2 = _mm256_add_epi64(s2, s11); - s3 = _mm256_add_epi64(s3, s15); - - x_ptr += 2; - y_ptr += 2; - } - // final reduction - const uint64_t H2 = precomp->h; - s8 = _mm256_set1_epi64x((UINT64_C(1) << H2) - 1); // MASK2 - s9 = _mm256_loadu_si256((__m256i*)precomp->s2l_pow_red); // S2L_POW_RED - s10 = _mm256_loadu_si256((__m256i*)precomp->s2h_pow_red); // S2H_POW_RED - //--- s0,s1 - s11 = _mm256_and_si256(s1, s8); - s12 = _mm256_srli_epi64(s1, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s0 = _mm256_add_epi64(s0, s13); - s0 = _mm256_add_epi64(s0, s14); - _mm256_storeu_si256(res_ptr + 0, s0); - //--- s2,s3 - s11 = _mm256_and_si256(s3, s8); - s12 = _mm256_srli_epi64(s3, H2); - s13 = _mm256_mul_epu32(s11, s9); - s14 = _mm256_mul_epu32(s12, s10); - s2 = _mm256_add_epi64(s2, s13); - s2 = _mm256_add_epi64(s2, s14); - _mm256_storeu_si256(res_ptr + 1, s2); -}
@@ -1,37 +0,0 @@
-#ifndef SPQLIOS_Q120_ARITHMETIC_DEF_H -#define SPQLIOS_Q120_ARITHMETIC_DEF_H - -#include <stdint.h> - -typedef struct _q120_mat1col_product_baa_precomp { - uint64_t h; - uint64_t h_pow_red[4]; -#ifndef NDEBUG - double res_bit_size; -#endif -} q120_mat1col_product_baa_precomp; - -typedef struct _q120_mat1col_product_bbb_precomp { - uint64_t h; - uint64_t s1h_pow_red[4]; - uint64_t s2l_pow_red[4]; - uint64_t s2h_pow_red[4]; - uint64_t s3l_pow_red[4]; - uint64_t s3h_pow_red[4]; - uint64_t s4l_pow_red[4]; - uint64_t s4h_pow_red[4]; -#ifndef NDEBUG - double res_bit_size; -#endif -} q120_mat1col_product_bbb_precomp; - -typedef struct _q120_mat1col_product_bbc_precomp { - uint64_t h; - uint64_t s2l_pow_red[4]; - uint64_t s2h_pow_red[4]; -#ifndef NDEBUG - double res_bit_size; -#endif -} q120_mat1col_product_bbc_precomp; - -#endif // SPQLIOS_Q120_ARITHMETIC_DEF_H
@@ -1,506 +0,0 @@
-#include <assert.h> -#include <math.h> -#include <stdint.h> - -#include "q120_arithmetic.h" -#include "q120_arithmetic_private.h" -#include "q120_common.h" - -#define MODQ(val, q) ((val) % (q)) - -double comp_bit_size_red(const uint64_t h, const uint64_t qs[4]) { - assert(h < 128); - double h_pow2_bs = 0; - for (uint64_t k = 0; k < 4; ++k) { - double t = log2((double)MODQ((__uint128_t)1 << h, qs[k])); - if (t > h_pow2_bs) h_pow2_bs = t; - } - return h_pow2_bs; -} - -double comp_bit_size_sum(const uint64_t n, const double* const bs) { - double s = 0; - for (uint64_t i = 0; i < n; ++i) { - s += pow(2, bs[i]); - } - return log2(s); -} - -void vec_mat1col_product_baa_precomp(q120_mat1col_product_baa_precomp* precomp) { - uint64_t qs[4] = {Q1, Q2, Q3, Q4}; - - double min_res_bs = 1000; - uint64_t min_h = -1; - - double ell_bs = log2((double)MAX_ELL); - for (uint64_t h = 1; h < 64; ++h) { - double h_pow2_bs = comp_bit_size_red(h, qs); - - const double bs[] = {h + ell_bs, 64 - h + ell_bs + h_pow2_bs}; - const double res_bs = comp_bit_size_sum(2, bs); - - if (min_res_bs > res_bs) { - min_res_bs = res_bs; - min_h = h; - } - } - - assert(min_res_bs < 64); - precomp->h = min_h; - for (uint64_t k = 0; k < 4; ++k) { - precomp->h_pow_red[k] = MODQ(UINT64_C(1) << precomp->h, qs[k]); - } -#ifndef NDEBUG - precomp->res_bit_size = min_res_bs; -#endif - // printf("AA %lu %lf\n", min_h, min_res_bs); -} - -EXPORT q120_mat1col_product_baa_precomp* q120_new_vec_mat1col_product_baa_precomp() { - q120_mat1col_product_baa_precomp* res = malloc(sizeof(q120_mat1col_product_baa_precomp)); - vec_mat1col_product_baa_precomp(res); - return res; -} - -EXPORT void q120_delete_vec_mat1col_product_baa_precomp(q120_mat1col_product_baa_precomp* addr) { free(addr); } - -EXPORT void q120_vec_mat1col_product_baa_ref(q120_mat1col_product_baa_precomp* precomp, const uint64_t ell, - q120b* const res, const q120a* const x, const q120a* const y) { - /** - * Algorithm: - * - res = acc1 + acc2 . ((2^H) % Q) - * - acc1 is the sum of H LSB of products x[i].y[i] - * - acc2 is the sum of 64-H MSB of products x[i]].y[i] - * - for l < 10k acc1 will have H + log2(10000) and acc2 64 - H + log2(10000) bits - * - final sum has max(H, 64 - H + bit_size((2^H) % Q)) + log2(10000) + 1 bits - */ - const uint64_t H = precomp->h; - const uint64_t MASK = (UINT64_C(1) << H) - 1; - - uint64_t acc1[4] = {0, 0, 0, 0}; // accumulate H least significant bits of product - uint64_t acc2[4] = {0, 0, 0, 0}; // accumulate 64 - H most significan bits of product - - const uint64_t* const x_ptr = (uint64_t*)x; - const uint64_t* const y_ptr = (uint64_t*)y; - - for (uint64_t i = 0; i < 4 * ell; i += 4) { - for (uint64_t j = 0; j < 4; ++j) { - uint64_t t = x_ptr[i + j] * y_ptr[i + j]; - acc1[j] += t & MASK; - acc2[j] += t >> H; - } - } - - uint64_t* const res_ptr = (uint64_t*)res; - for (uint64_t j = 0; j < 4; ++j) { - res_ptr[j] = acc1[j] + acc2[j] * precomp->h_pow_red[j]; - assert(log2(res_ptr[j]) < precomp->res_bit_size); - } -} - -void vec_mat1col_product_bbb_precomp(q120_mat1col_product_bbb_precomp* precomp) { - uint64_t qs[4] = {Q1, Q2, Q3, Q4}; - - double ell_bs = log2((double)MAX_ELL); - double min_res_bs = 1000; - uint64_t min_h = -1; - - const double s1_bs = 32 + ell_bs; - const double s2_bs = 32 + ell_bs + log2(3); - const double s3_bs = 32 + ell_bs + log2(3); - const double s4_bs = 32 + ell_bs; - for (uint64_t h = 16; h < 32; ++h) { - const double s1l_bs = h; - const double s1h_bs = (s1_bs - h) + comp_bit_size_red(h, qs); - const double s2l_bs = h + comp_bit_size_red(32, qs); - const double s2h_bs = (s2_bs - h) + comp_bit_size_red(32 + h, qs); - const double s3l_bs = h + comp_bit_size_red(64, qs); - const double s3h_bs = (s3_bs - h) + comp_bit_size_red(64 + h, qs); - const double s4l_bs = h + comp_bit_size_red(96, qs); - const double s4h_bs = (s4_bs - h) + comp_bit_size_red(96 + h, qs); - - const double bs[] = {s1l_bs, s1h_bs, s2l_bs, s2h_bs, s3l_bs, s3h_bs, s4l_bs, s4h_bs}; - const double res_bs = comp_bit_size_sum(8, bs); - - if (min_res_bs > res_bs) { - min_res_bs = res_bs; - min_h = h; - } - } - - assert(min_res_bs < 64); - precomp->h = min_h; - for (uint64_t k = 0; k < 4; ++k) { - precomp->s1h_pow_red[k] = UINT64_C(1) << precomp->h; // 2^24 - precomp->s2l_pow_red[k] = MODQ(UINT64_C(1) << 32, qs[k]); // 2^32 - precomp->s2h_pow_red[k] = MODQ(precomp->s2l_pow_red[k] * precomp->s1h_pow_red[k], qs[k]); // 2^(32+24) - precomp->s3l_pow_red[k] = MODQ(precomp->s2l_pow_red[k] * precomp->s2l_pow_red[k], qs[k]); // 2^64 = 2^(32+32) - precomp->s3h_pow_red[k] = MODQ(precomp->s3l_pow_red[k] * precomp->s1h_pow_red[k], qs[k]); // 2^(64+24) - precomp->s4l_pow_red[k] = MODQ(precomp->s3l_pow_red[k] * precomp->s2l_pow_red[k], qs[k]); // 2^96 = 2^(64+32) - precomp->s4h_pow_red[k] = MODQ(precomp->s4l_pow_red[k] * precomp->s1h_pow_red[k], qs[k]); // 2^(96+24) - } -// printf("AA %lu %lf\n", min_h, min_res_bs); -#ifndef NDEBUG - precomp->res_bit_size = min_res_bs; -#endif -} - -EXPORT q120_mat1col_product_bbb_precomp* q120_new_vec_mat1col_product_bbb_precomp() { - q120_mat1col_product_bbb_precomp* res = malloc(sizeof(q120_mat1col_product_bbb_precomp)); - vec_mat1col_product_bbb_precomp(res); - return res; -} - -EXPORT void q120_delete_vec_mat1col_product_bbb_precomp(q120_mat1col_product_bbb_precomp* addr) { free(addr); } - -EXPORT void q120_vec_mat1col_product_bbb_ref(q120_mat1col_product_bbb_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120b* const y) { - /** - * Algorithm: - * 1. Split x_i and y_i in 2 32-bit parts and compute the cross-products: - * - x_i = xl_i + xh_i . 2^32 - * - y_i = yl_i + yh_i . 2^32 - * - A_i = xl_i . yl_i - * - B_i = xl_i . yh_i - * - C_i = xh_i . yl_i - * - D_i = xh_i . yh_i - * - we have x_i . y_i == A_i + (B_i + C_i) . 2^32 + D_i . 2^64 - * 2. Split A_i, B_i, C_i and D_i into 2 32-bit parts - * - A_i = Al_i + Ah_i . 2^32 - * - B_i = Bl_i + Bh_i . 2^32 - * - C_i = Cl_i + Ch_i . 2^32 - * - D_i = Dl_i + Dh_i . 2^32 - * 3. Compute the sums: - * - S1 = \sum Al_i - * - S2 = \sum (Ah_i + Bl_i + Cl_i) - * - S3 = \sum (Bh_i + Ch_i + Dl_i) - * - S4 = \sum Dh_i - * - here S1, S4 have 32 + log2(ell) bits and S2, S3 have 32 + log2(ell) + - * log2(3) bits - * - for ell == 10000 S2, S3 have < 47 bits - * 4. Split S1, S2, S3 and S4 in 2 24-bit parts (24 = ceil(47/2)) - * - S1 = S1l + S1h . 2^24 - * - S2 = S2l + S2h . 2^24 - * - S3 = S3l + S3h . 2^24 - * - S4 = S4l + S4h . 2^24 - * 5. Compute final result as: - * - \sum x_i . y_i = S1l + S1h . 2^24 - * + S2l . 2^32 + S2h . 2^(32+24) - * + S3l . 2^64 + S3h . 2^(64 + 24) - * + S4l . 2^96 + S4l . 2^(96+24) - * - here the powers of 2 are reduced modulo the primes Q before - * multiplications - * - the result will be on 24 + 3 + bit size of primes Q - */ - const uint64_t H1 = 32; - const uint64_t MASK1 = (UINT64_C(1) << H1) - 1; - - uint64_t s1[4] = {0, 0, 0, 0}; - uint64_t s2[4] = {0, 0, 0, 0}; - uint64_t s3[4] = {0, 0, 0, 0}; - uint64_t s4[4] = {0, 0, 0, 0}; - - const uint64_t* const x_ptr = (uint64_t*)x; - const uint64_t* const y_ptr = (uint64_t*)y; - - for (uint64_t i = 0; i < 4 * ell; i += 4) { - for (uint64_t j = 0; j < 4; ++j) { - const uint64_t xl = x_ptr[i + j] & MASK1; - const uint64_t xh = x_ptr[i + j] >> H1; - const uint64_t yl = y_ptr[i + j] & MASK1; - const uint64_t yh = y_ptr[i + j] >> H1; - - const uint64_t a = xl * yl; - const uint64_t al = a & MASK1; - const uint64_t ah = a >> H1; - - const uint64_t b = xl * yh; - const uint64_t bl = b & MASK1; - const uint64_t bh = b >> H1; - - const uint64_t c = xh * yl; - const uint64_t cl = c & MASK1; - const uint64_t ch = c >> H1; - - const uint64_t d = xh * yh; - const uint64_t dl = d & MASK1; - const uint64_t dh = d >> H1; - - s1[j] += al; - s2[j] += ah + bl + cl; - s3[j] += bh + ch + dl; - s4[j] += dh; - } - } - - const uint64_t H2 = precomp->h; - const uint64_t MASK2 = (UINT64_C(1) << H2) - 1; - - uint64_t* const res_ptr = (uint64_t*)res; - for (uint64_t j = 0; j < 4; ++j) { - const uint64_t s1l = s1[j] & MASK2; - const uint64_t s1h = s1[j] >> H2; - const uint64_t s2l = s2[j] & MASK2; - const uint64_t s2h = s2[j] >> H2; - const uint64_t s3l = s3[j] & MASK2; - const uint64_t s3h = s3[j] >> H2; - const uint64_t s4l = s4[j] & MASK2; - const uint64_t s4h = s4[j] >> H2; - - uint64_t t = s1l; - t += s1h * precomp->s1h_pow_red[j]; - t += s2l * precomp->s2l_pow_red[j]; - t += s2h * precomp->s2h_pow_red[j]; - t += s3l * precomp->s3l_pow_red[j]; - t += s3h * precomp->s3h_pow_red[j]; - t += s4l * precomp->s4l_pow_red[j]; - t += s4h * precomp->s4h_pow_red[j]; - - res_ptr[j] = t; - assert(log2(res_ptr[j]) < precomp->res_bit_size); - } -} - -void vec_mat1col_product_bbc_precomp(q120_mat1col_product_bbc_precomp* precomp) { - uint64_t qs[4] = {Q1, Q2, Q3, Q4}; - - double min_res_bs = 1000; - uint64_t min_h = -1; - - double pow2_32_bs = comp_bit_size_red(32, qs); - - double ell_bs = log2((double)MAX_ELL); - double s1_bs = 32 + ell_bs; - for (uint64_t h = 16; h < 32; ++h) { - double s2l_bs = pow2_32_bs + h; - double s2h_bs = s1_bs - h + comp_bit_size_red(32 + h, qs); - - const double bs[] = {s1_bs, s2l_bs, s2h_bs}; - const double res_bs = comp_bit_size_sum(3, bs); - - if (min_res_bs > res_bs) { - min_res_bs = res_bs; - min_h = h; - } - } - - assert(min_res_bs < 64); - precomp->h = min_h; - for (uint64_t k = 0; k < 4; ++k) { - precomp->s2l_pow_red[k] = MODQ(UINT64_C(1) << 32, qs[k]); - precomp->s2h_pow_red[k] = MODQ(UINT64_C(1) << (32 + precomp->h), qs[k]); - } -#ifndef NDEBUG - precomp->res_bit_size = min_res_bs; -#endif - // printf("AA %lu %lf\n", min_h, min_res_bs); -} - -EXPORT q120_mat1col_product_bbc_precomp* q120_new_vec_mat1col_product_bbc_precomp() { - q120_mat1col_product_bbc_precomp* res = malloc(sizeof(q120_mat1col_product_bbc_precomp)); - vec_mat1col_product_bbc_precomp(res); - return res; -} - -EXPORT void q120_delete_vec_mat1col_product_bbc_precomp(q120_mat1col_product_bbc_precomp* addr) { free(addr); } - -EXPORT void q120_vec_mat1col_product_bbc_ref_old(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - /** - * Algorithm: - * 0. We have - * - y0_i == y_i % Q and y1_i == (y_i . 2^32) % Q - * 1. Split x_i in 2 32-bit parts and compute the cross-products: - * - x_i = xl_i + xh_i . 2^32 - * - A_i = xl_i . y1_i - * - B_i = xh_i . y2_i - * - we have x_i . y_i == A_i + B_i - * 2. Split A_i and B_i into 2 32-bit parts - * - A_i = Al_i + Ah_i . 2^32 - * - B_i = Bl_i + Bh_i . 2^32 - * 3. Compute the sums: - * - S1 = \sum Al_i + Bl_i - * - S2 = \sum Ah_i + Bh_i - * - here S1 and S2 have 32 + log2(ell) bits - * - for ell == 10000 S1, S2 have < 46 bits - * 4. Split S2 in 27-bit and 19-bit parts (27+19 == 46) - * - S2 = S2l + S2h . 2^27 - * 5. Compute final result as: - * - \sum x_i . y_i = S1 + S2l . 2^32 + S2h . 2^(32+27) - * - here the powers of 2 are reduced modulo the primes Q before - * multiplications - * - the result will be on < 52 bits - */ - - const uint64_t H1 = 32; - const uint64_t MASK1 = (UINT64_C(1) << H1) - 1; - - uint64_t s1[4] = {0, 0, 0, 0}; - uint64_t s2[4] = {0, 0, 0, 0}; - - const uint64_t* const x_ptr = (uint64_t*)x; - const uint32_t* const y_ptr = (uint32_t*)y; - - for (uint64_t i = 0; i < 4 * ell; i += 4) { - for (uint64_t j = 0; j < 4; ++j) { - const uint64_t xl = x_ptr[i + j] & MASK1; - const uint64_t xh = x_ptr[i + j] >> H1; - const uint64_t y0 = y_ptr[2 * (i + j)]; - const uint64_t y1 = y_ptr[2 * (i + j) + 1]; - - const uint64_t a = xl * y0; - const uint64_t al = a & MASK1; - const uint64_t ah = a >> H1; - - const uint64_t b = xh * y1; - const uint64_t bl = b & MASK1; - const uint64_t bh = b >> H1; - - s1[j] += al + bl; - s2[j] += ah + bh; - } - } - - const uint64_t H2 = precomp->h; - const uint64_t MASK2 = (UINT64_C(1) << H2) - 1; - - uint64_t* const res_ptr = (uint64_t*)res; - for (uint64_t k = 0; k < 4; ++k) { - const uint64_t s2l = s2[k] & MASK2; - const uint64_t s2h = s2[k] >> H2; - - uint64_t t = s1[k]; - t += s2l * precomp->s2l_pow_red[k]; - t += s2h * precomp->s2h_pow_red[k]; - - res_ptr[k] = t; - assert(log2(res_ptr[k]) < precomp->res_bit_size); - } -} - -static __always_inline void accum_mul_q120_bc(uint64_t res[8], // - const uint32_t x_layb[8], const uint32_t y_layc[8]) { - for (uint64_t i = 0; i < 4; ++i) { - static const uint64_t MASK32 = 0xFFFFFFFFUL; - uint64_t x_lo = x_layb[2 * i]; - uint64_t x_hi = x_layb[2 * i + 1]; - uint64_t y_lo = y_layc[2 * i]; - uint64_t y_hi = y_layc[2 * i + 1]; - uint64_t xy_lo = x_lo * y_lo; - uint64_t xy_hi = x_hi * y_hi; - res[2 * i] += (xy_lo & MASK32) + (xy_hi & MASK32); - res[2 * i + 1] += (xy_lo >> 32) + (xy_hi >> 32); - } -} - -static __always_inline void accum_to_q120b(uint64_t res[4], // - const uint64_t s[8], const q120_mat1col_product_bbc_precomp* precomp) { - const uint64_t H2 = precomp->h; - const uint64_t MASK2 = (UINT64_C(1) << H2) - 1; - for (uint64_t k = 0; k < 4; ++k) { - const uint64_t s2l = s[2 * k + 1] & MASK2; - const uint64_t s2h = s[2 * k + 1] >> H2; - uint64_t t = s[2 * k]; - t += s2l * precomp->s2l_pow_red[k]; - t += s2h * precomp->s2h_pow_red[k]; - res[k] = t; - assert(log2(res[k]) < precomp->res_bit_size); - } -} - -EXPORT void q120_vec_mat1col_product_bbc_ref(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - uint64_t s[8] = {0, 0, 0, 0, 0, 0, 0, 0}; - - const uint32_t(*const x_ptr)[8] = (const uint32_t(*const)[8])x; - const uint32_t(*const y_ptr)[8] = (const uint32_t(*const)[8])y; - - for (uint64_t i = 0; i < ell; i++) { - accum_mul_q120_bc(s, x_ptr[i], y_ptr[i]); - } - accum_to_q120b((uint64_t*)res, s, precomp); -} - -EXPORT void q120x2_vec_mat1col_product_bbc_ref(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - uint64_t s[2][16] = {0}; - - const uint32_t(*const x_ptr)[2][8] = (const uint32_t(*const)[2][8])x; - const uint32_t(*const y_ptr)[2][8] = (const uint32_t(*const)[2][8])y; - uint64_t(*re)[4] = (uint64_t(*)[4])res; - - for (uint64_t i = 0; i < ell; i++) { - accum_mul_q120_bc(s[0], x_ptr[i][0], y_ptr[i][0]); - accum_mul_q120_bc(s[1], x_ptr[i][1], y_ptr[i][1]); - } - accum_to_q120b(re[0], s[0], precomp); - accum_to_q120b(re[1], s[1], precomp); -} - -EXPORT void q120x2_vec_mat2cols_product_bbc_ref(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y) { - uint64_t s[4][16] = {0}; - - const uint32_t(*const x_ptr)[2][8] = (const uint32_t(*const)[2][8])x; - const uint32_t(*const y_ptr)[4][8] = (const uint32_t(*const)[4][8])y; - uint64_t(*re)[4] = (uint64_t(*)[4])res; - - for (uint64_t i = 0; i < ell; i++) { - accum_mul_q120_bc(s[0], x_ptr[i][0], y_ptr[i][0]); - accum_mul_q120_bc(s[1], x_ptr[i][1], y_ptr[i][1]); - accum_mul_q120_bc(s[2], x_ptr[i][0], y_ptr[i][2]); - accum_mul_q120_bc(s[3], x_ptr[i][1], y_ptr[i][3]); - } - accum_to_q120b(re[0], s[0], precomp); - accum_to_q120b(re[1], s[1], precomp); - accum_to_q120b(re[2], s[2], precomp); - accum_to_q120b(re[3], s[3], precomp); -} - -EXPORT void q120x2_extract_1blk_from_q120b_ref(uint64_t nn, uint64_t blk, - q120x2b* const dst, // 8 doubles - const q120b* const src // a q120b vector -) { - const uint64_t* in = (uint64_t*)src; - uint64_t* out = (uint64_t*)dst; - for (uint64_t i = 0; i < 8; ++i) { - out[i] = in[8 * blk + i]; - } -} - -// function on layout c is the exact same as on layout b -#ifdef __APPLE__ -#pragma weak q120x2_extract_1blk_from_q120c_ref = q120x2_extract_1blk_from_q120b_ref -#else -EXPORT void q120x2_extract_1blk_from_q120c_ref(uint64_t nn, uint64_t blk, - q120x2c* const dst, // 8 doubles - const q120c* const src // a q120c vector - ) __attribute__((alias("q120x2_extract_1blk_from_q120b_ref"))); -#endif - -EXPORT void q120x2_extract_1blk_from_contiguous_q120b_ref( - uint64_t nn, uint64_t nrows, uint64_t blk, - q120x2b* const dst, // nrows * 2 q120 - const q120b* const src // a contiguous array of nrows q120b vectors -) { - const uint64_t* in = (uint64_t*)src; - uint64_t* out = (uint64_t*)dst; - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t i = 0; i < 8; ++i) { - out[i] = in[8 * blk + i]; - } - in += 4 * nn; - out += 8; - } -} - -EXPORT void q120x2b_save_1blk_to_q120b_ref(uint64_t nn, uint64_t blk, - q120b* dest, // 1 reim vector of length m - const q120x2b* src // 8 doubles -) { - const uint64_t* in = (uint64_t*)src; - uint64_t* out = (uint64_t*)dest; - for (uint64_t i = 0; i < 8; ++i) { - out[8 * blk + i] = in[i]; - } -}
@@ -1,111 +0,0 @@
-#include <assert.h> -#include <math.h> -#include <stdint.h> - -#include "q120_arithmetic.h" -#include "q120_common.h" - -EXPORT void q120_add_bbb_simple(uint64_t nn, q120b* const res, const q120b* const x, const q120b* const y) { - const uint64_t* x_u64 = (uint64_t*)x; - const uint64_t* y_u64 = (uint64_t*)y; - uint64_t* res_u64 = (uint64_t*)res; - for (uint64_t i = 0; i < 4 * nn; i += 4) { - res_u64[i + 0] = x_u64[i + 0] % ((uint64_t)Q1 << 33) + y_u64[i + 0] % ((uint64_t)Q1 << 33); - res_u64[i + 1] = x_u64[i + 1] % ((uint64_t)Q2 << 33) + y_u64[i + 1] % ((uint64_t)Q2 << 33); - res_u64[i + 2] = x_u64[i + 2] % ((uint64_t)Q3 << 33) + y_u64[i + 2] % ((uint64_t)Q3 << 33); - res_u64[i + 3] = x_u64[i + 3] % ((uint64_t)Q4 << 33) + y_u64[i + 3] % ((uint64_t)Q4 << 33); - } -} - -EXPORT void q120_add_ccc_simple(uint64_t nn, q120c* const res, const q120c* const x, const q120c* const y) { - const uint32_t* x_u32 = (uint32_t*)x; - const uint32_t* y_u32 = (uint32_t*)y; - uint32_t* res_u32 = (uint32_t*)res; - for (uint64_t i = 0; i < 8 * nn; i += 8) { - res_u32[i + 0] = (uint32_t)(((uint64_t)x_u32[i + 0] + (uint64_t)y_u32[i + 0]) % Q1); - res_u32[i + 1] = (uint32_t)(((uint64_t)x_u32[i + 1] + (uint64_t)y_u32[i + 1]) % Q1); - res_u32[i + 2] = (uint32_t)(((uint64_t)x_u32[i + 2] + (uint64_t)y_u32[i + 2]) % Q2); - res_u32[i + 3] = (uint32_t)(((uint64_t)x_u32[i + 3] + (uint64_t)y_u32[i + 3]) % Q2); - res_u32[i + 4] = (uint32_t)(((uint64_t)x_u32[i + 4] + (uint64_t)y_u32[i + 4]) % Q3); - res_u32[i + 5] = (uint32_t)(((uint64_t)x_u32[i + 5] + (uint64_t)y_u32[i + 5]) % Q3); - res_u32[i + 6] = (uint32_t)(((uint64_t)x_u32[i + 6] + (uint64_t)y_u32[i + 6]) % Q4); - res_u32[i + 7] = (uint32_t)(((uint64_t)x_u32[i + 7] + (uint64_t)y_u32[i + 7]) % Q4); - } -} - -EXPORT void q120_c_from_b_simple(uint64_t nn, q120c* const res, const q120b* const x) { - const uint64_t* x_u64 = (uint64_t*)x; - uint32_t* res_u32 = (uint32_t*)res; - for (uint64_t i = 0, j = 0; i < 4 * nn; i += 4, j += 8) { - res_u32[j + 0] = x_u64[i + 0] % Q1; - res_u32[j + 1] = ((uint64_t)res_u32[j + 0] << 32) % Q1; - res_u32[j + 2] = x_u64[i + 1] % Q2; - res_u32[j + 3] = ((uint64_t)res_u32[j + 2] << 32) % Q2; - res_u32[j + 4] = x_u64[i + 2] % Q3; - res_u32[j + 5] = ((uint64_t)res_u32[j + 4] << 32) % Q3; - res_u32[j + 6] = x_u64[i + 3] % Q4; - res_u32[j + 7] = ((uint64_t)res_u32[j + 6] << 32) % Q4; - } -} - -EXPORT void q120_b_from_znx64_simple(uint64_t nn, q120b* const res, const int64_t* const x) { - static const int64_t MASK_HI = INT64_C(0x8000000000000000); - static const int64_t MASK_LO = ~MASK_HI; - static const uint64_t OQ[4] = { - (Q1 - (UINT64_C(0x8000000000000000) % Q1)), - (Q2 - (UINT64_C(0x8000000000000000) % Q2)), - (Q3 - (UINT64_C(0x8000000000000000) % Q3)), - (Q4 - (UINT64_C(0x8000000000000000) % Q4)), - }; - uint64_t* res_u64 = (uint64_t*)res; - for (uint64_t i = 0, j = 0; j < nn; i += 4, ++j) { - uint64_t xj_lo = x[j] & MASK_LO; - uint64_t xj_hi = x[j] & MASK_HI; - res_u64[i + 0] = xj_lo + (xj_hi ? OQ[0] : 0); - res_u64[i + 1] = xj_lo + (xj_hi ? OQ[1] : 0); - res_u64[i + 2] = xj_lo + (xj_hi ? OQ[2] : 0); - res_u64[i + 3] = xj_lo + (xj_hi ? OQ[3] : 0); - } -} - -static int64_t posmod(int64_t x, int64_t q) { - int64_t t = x % q; - if (t < 0) - return t + q; - else - return t; -} - -EXPORT void q120_c_from_znx64_simple(uint64_t nn, q120c* const res, const int64_t* const x) { - uint32_t* res_u32 = (uint32_t*)res; - for (uint64_t i = 0, j = 0; j < nn; i += 8, ++j) { - res_u32[i + 0] = posmod(x[j], Q1); - res_u32[i + 1] = ((uint64_t)res_u32[i + 0] << 32) % Q1; - res_u32[i + 2] = posmod(x[j], Q2); - res_u32[i + 3] = ((uint64_t)res_u32[i + 2] << 32) % Q2; - res_u32[i + 4] = posmod(x[j], Q3); - res_u32[i + 5] = ((uint64_t)res_u32[i + 4] << 32) % Q3; - res_u32[i + 6] = posmod(x[j], Q4); - res_u32[i + 7] = ((uint64_t)res_u32[i + 6] << 32) % Q4; - ; - } -} - -EXPORT void q120_b_to_znx128_simple(uint64_t nn, __int128_t* const res, const q120b* const x) { - static const __int128_t Q = (__int128_t)Q1 * Q2 * Q3 * Q4; - static const __int128_t Qm1 = (__int128_t)Q2 * Q3 * Q4; - static const __int128_t Qm2 = (__int128_t)Q1 * Q3 * Q4; - static const __int128_t Qm3 = (__int128_t)Q1 * Q2 * Q4; - static const __int128_t Qm4 = (__int128_t)Q1 * Q2 * Q3; - - const uint64_t* x_u64 = (uint64_t*)x; - for (uint64_t i = 0, j = 0; j < nn; i += 4, ++j) { - __int128_t tmp = 0; - tmp += (((x_u64[i + 0] % Q1) * Q1_CRT_CST) % Q1) * Qm1; - tmp += (((x_u64[i + 1] % Q2) * Q2_CRT_CST) % Q2) * Qm2; - tmp += (((x_u64[i + 2] % Q3) * Q3_CRT_CST) % Q3) * Qm3; - tmp += (((x_u64[i + 3] % Q4) * Q4_CRT_CST) % Q4) * Qm4; - tmp %= Q; - res[j] = (tmp >= (Q + 1) / 2) ? tmp - Q : tmp; - } -}
@@ -1,94 +0,0 @@
-#ifndef SPQLIOS_Q120_COMMON_H -#define SPQLIOS_Q120_COMMON_H - -#include <stdint.h> - -#if !defined(SPQLIOS_Q120_USE_29_BIT_PRIMES) && !defined(SPQLIOS_Q120_USE_30_BIT_PRIMES) && \ - !defined(SPQLIOS_Q120_USE_31_BIT_PRIMES) -#define SPQLIOS_Q120_USE_30_BIT_PRIMES -#endif - -/** - * 29-bit primes and 2*2^16 roots of unity - */ -#ifdef SPQLIOS_Q120_USE_29_BIT_PRIMES -#define Q1 ((1u << 29) - 2 * (1u << 17) + 1) -#define OMEGA1 78289835 -#define Q1_CRT_CST 301701286 // (Q2*Q3*Q4)^-1 mod Q1 - -#define Q2 ((1u << 29) - 5 * (1u << 17) + 1) -#define OMEGA2 178519192 -#define Q2_CRT_CST 536020447 // (Q1*Q3*Q4)^-1 mod Q2 - -#define Q3 ((1u << 29) - 26 * (1u << 17) + 1) -#define OMEGA3 483889678 -#define Q3_CRT_CST 86367873 // (Q1*Q2*Q4)^-1 mod Q3 - -#define Q4 ((1u << 29) - 35 * (1u << 17) + 1) -#define OMEGA4 239808033 -#define Q4_CRT_CST 147030781 // (Q1*Q2*Q3)^-1 mod Q4 -#endif - -/** - * 30-bit primes and 2*2^16 roots of unity - */ -#ifdef SPQLIOS_Q120_USE_30_BIT_PRIMES -#define Q1 ((1u << 30) - 2 * (1u << 17) + 1) -#define OMEGA1 1070907127 -#define Q1_CRT_CST 43599465 // (Q2*Q3*Q4)^-1 mod Q1 - -#define Q2 ((1u << 30) - 17 * (1u << 17) + 1) -#define OMEGA2 315046632 -#define Q2_CRT_CST 292938863 // (Q1*Q3*Q4)^-1 mod Q2 - -#define Q3 ((1u << 30) - 23 * (1u << 17) + 1) -#define OMEGA3 309185662 -#define Q3_CRT_CST 594011630 // (Q1*Q2*Q4)^-1 mod Q3 - -#define Q4 ((1u << 30) - 42 * (1u << 17) + 1) -#define OMEGA4 846468380 -#define Q4_CRT_CST 140177212 // (Q1*Q2*Q3)^-1 mod Q4 -#endif - -/** - * 31-bit primes and 2*2^16 roots of unity - */ -#ifdef SPQLIOS_Q120_USE_31_BIT_PRIMES -#define Q1 ((1u << 31) - 1 * (1u << 17) + 1) -#define OMEGA1 1615402923 -#define Q1_CRT_CST 1811422063 // (Q2*Q3*Q4)^-1 mod Q1 - -#define Q2 ((1u << 31) - 4 * (1u << 17) + 1) -#define OMEGA2 1137738560 -#define Q2_CRT_CST 2093150204 // (Q1*Q3*Q4)^-1 mod Q2 - -#define Q3 ((1u << 31) - 11 * (1u << 17) + 1) -#define OMEGA3 154880552 -#define Q3_CRT_CST 164149010 // (Q1*Q2*Q4)^-1 mod Q3 - -#define Q4 ((1u << 31) - 23 * (1u << 17) + 1) -#define OMEGA4 558784885 -#define Q4_CRT_CST 225197446 // (Q1*Q2*Q3)^-1 mod Q4 -#endif - -static const uint32_t PRIMES_VEC[4] = {Q1, Q2, Q3, Q4}; -static const uint32_t OMEGAS_VEC[4] = {OMEGA1, OMEGA2, OMEGA3, OMEGA4}; - -#define MAX_ELL 10000 - -// each number x mod Q120 is represented by uint64_t[4] with (non-unique) values (x mod q1, x mod q2,x mod q3,x mod q4), -// each between [0 and 2^32-1] -typedef struct _q120a q120a; - -// each number x mod Q120 is represented by uint64_t[4] with (non-unique) values (x mod q1, x mod q2,x mod q3,x mod q4), -// each between [0 and 2^64-1] -typedef struct _q120b q120b; - -// each number x mod Q120 is represented by uint32_t[8] with values (x mod q1, 2^32x mod q1, x mod q2, 2^32.x mod q2, x -// mod q3, 2^32.x mod q3, x mod q4, 2^32.x mod q4) each between [0 and 2^32-1] -typedef struct _q120c q120c; - -typedef struct _q120x2b q120x2b; -typedef struct _q120x2c q120x2c; - -#endif // SPQLIOS_Q120_COMMON_H
@@ -1,5 +0,0 @@
-#include "q120_ntt_private.h" - -EXPORT void q120_ntt_bb_avx2(const q120_ntt_precomp* const precomp, q120b* const data) { UNDEFINED(); } - -EXPORT void q120_intt_bb_avx2(const q120_ntt_precomp* const precomp, q120b* const data) { UNDEFINED(); }
@@ -1,340 +0,0 @@
-#include <assert.h> -#include <inttypes.h> -#include <math.h> -#include <stdint.h> - -#include "q120_ntt_private.h" - -q120_ntt_precomp* new_precomp(const uint64_t n) { - q120_ntt_precomp* precomp = malloc(sizeof(*precomp)); - precomp->n = n; - - assert(n && !(n & (n - 1)) && n <= (1 << 16)); // n is a power of 2 smaller than 2^16 - const uint64_t logN = ceil(log2(n)); - precomp->level_metadata = malloc((logN + 2) * sizeof(*precomp->level_metadata)); - - precomp->powomega = spqlios_alloc_custom_align(32, 4 * 2 * n * sizeof(*(precomp->powomega))); - - return precomp; -} - -uint32_t modq_pow(const uint32_t x, const int64_t n, const uint32_t q) { - uint64_t np = (n % (q - 1) + q - 1) % (q - 1); - - uint64_t val_pow = x; - uint64_t res = 1; - while (np != 0) { - if (np & 1) res = (res * val_pow) % q; - val_pow = (val_pow * val_pow) % q; - np >>= 1; - } - return res; -} - -void fill_omegas(const uint64_t n, uint32_t omegas[4]) { - for (uint64_t k = 0; k < 4; ++k) { - omegas[k] = modq_pow(OMEGAS_VEC[k], (1 << 16) / n, PRIMES_VEC[k]); - } - -#ifndef NDEBUG - - const uint64_t logQ = ceil(log2(PRIMES_VEC[0])); - for (int k = 1; k < 4; ++k) { - if (logQ != ceil(log2(PRIMES_VEC[k]))) { - fprintf(stderr, "The 4 primes must have the same bit-size\n"); - exit(-1); - } - } - - // check if each omega is a 2.n primitive root of unity - for (uint64_t k = 0; k < 4; ++k) { - assert(modq_pow(omegas[k], 2 * n, PRIMES_VEC[k]) == 1); - for (uint64_t i = 1; i < 2 * n; ++i) { - assert(modq_pow(omegas[k], i, PRIMES_VEC[k]) != 1); - } - } - - if (logQ > 31) { - fprintf(stderr, "Modulus q bit-size is larger than 30 bit\n"); - exit(-1); - } -#endif -} - -uint64_t fill_reduction_meta(const uint64_t bs_start, q120_ntt_reduc_step_precomp* reduc_metadata) { - // fill reduction metadata - uint64_t bs_after_reduc = -1; - { - uint64_t min_h = -1; - - for (uint64_t h = bs_start / 2; h < bs_start; ++h) { - uint64_t t = 0; - for (uint64_t k = 0; k < 4; ++k) { - const uint64_t t1 = bs_start - h + (uint64_t)ceil(log2((UINT64_C(1) << h) % PRIMES_VEC[k])); - const uint64_t t2 = UINT64_C(1) + ((t1 > h) ? t1 : h); - if (t < t2) t = t2; - } - if (t < bs_after_reduc) { - min_h = h; - bs_after_reduc = t; - } - } - - reduc_metadata->h = min_h; - reduc_metadata->mask = (UINT64_C(1) << min_h) - 1; - for (uint64_t k = 0; k < 4; ++k) { - reduc_metadata->modulo_red_cst[k] = (UINT64_C(1) << min_h) % PRIMES_VEC[k]; - } - - assert(bs_after_reduc < 64); - } - - return bs_after_reduc; -} - -uint64_t round_up_half_n(const uint64_t n) { return (n + 1) / 2; } - -EXPORT q120_ntt_precomp* q120_new_ntt_bb_precomp(const uint64_t n) { - uint32_t omega_vec[4]; - fill_omegas(n, omega_vec); - - const uint64_t logQ = ceil(log2(PRIMES_VEC[0])); - - q120_ntt_precomp* precomp = new_precomp(n); - - uint64_t bs = precomp->input_bit_size = 64; - - LOG("NTT parameters:\n"); - LOG("\tsize = %" PRIu64 "\n", n) - LOG("\tlogQ = %" PRIu64 "\n", logQ); - LOG("\tinput bit-size = %" PRIu64 "\n", bs); - - if (n == 1) return precomp; - - // fill reduction metadata - uint64_t bs_after_reduc = fill_reduction_meta(bs, &(precomp->reduc_metadata)); - - // forward metadata - q120_ntt_step_precomp* level_metadata_ptr = precomp->level_metadata; - - // first level a_k.omega^k - { - const uint64_t half_bs = (bs + 1) / 2; - level_metadata_ptr->half_bs = half_bs; - level_metadata_ptr->mask = (UINT64_C(1) << half_bs) - UINT64_C(1); - level_metadata_ptr->bs = bs = half_bs + logQ + 1; - LOG("\tlevel %6" PRIu64 " output bit-size = %" PRIu64 " (a_k.omega^k) \n", n, bs); - level_metadata_ptr++; - } - - for (uint64_t nn = n; nn >= 4; nn /= 2) { - level_metadata_ptr->reduce = (bs == 64); - if (level_metadata_ptr->reduce) { - bs = bs_after_reduc; - LOG("\treduce output bit-size = %" PRIu64 "\n", bs); - } - - for (int k = 0; k < 4; ++k) level_metadata_ptr->q2bs[k] = (uint64_t)PRIMES_VEC[k] << (bs - logQ); - - double bs_1 = bs + 1; // bit-size of term a+b or a-b - - const uint64_t half_bs = round_up_half_n(bs_1); - uint64_t bs_2 = half_bs + logQ + 1; // bit-size of term (a-b).omega^k - bs = (bs_1 > bs_2) ? bs_1 : bs_2; - assert(bs <= 64); - - level_metadata_ptr->bs = bs; - level_metadata_ptr->half_bs = half_bs; - level_metadata_ptr->mask = (UINT64_C(1) << half_bs) - UINT64_C(1); - level_metadata_ptr++; - - LOG("\tlevel %6" PRIu64 " output bit-size = %" PRIu64 "\n", nn / 2, bs); - } - - // last level (a-b, a+b) - { - level_metadata_ptr->reduce = (bs == 64); - if (level_metadata_ptr->reduce) { - bs = bs_after_reduc; - LOG("\treduce output bit-size = %" PRIu64 "\n", bs); - } - for (int k = 0; k < 4; ++k) level_metadata_ptr->q2bs[k] = ((uint64_t)PRIMES_VEC[k] << (bs - logQ)); - level_metadata_ptr->bs = ++bs; - level_metadata_ptr->half_bs = level_metadata_ptr->mask = UINT64_C(0); // not used - - LOG("\tlevel %6" PRIu64 " output bit-size = %" PRIu64 "\n", UINT64_C(1), bs); - } - precomp->output_bit_size = bs; - - // omega powers - uint64_t* powomega = malloc(sizeof(*powomega) * 2 * n); - for (uint64_t k = 0; k < 4; ++k) { - const uint64_t q = PRIMES_VEC[k]; - - for (uint64_t i = 0; i < 2 * n; ++i) { - powomega[i] = modq_pow(omega_vec[k], i, q); - } - - uint64_t* powomega_ptr = precomp->powomega + k; - level_metadata_ptr = precomp->level_metadata; - - { - // const uint64_t hpow = UINT64_C(1) << level_metadata_ptr->half_bs; - for (uint64_t i = 0; i < n; ++i) { - uint64_t t = powomega[i]; - uint64_t t1 = (t << level_metadata_ptr->half_bs) % q; - powomega_ptr[4 * i] = (t1 << 32) + t; - } - powomega_ptr += 4 * n; - level_metadata_ptr++; - } - - for (uint64_t nn = n; nn >= 4; nn /= 2) { - const uint64_t halfnn = nn / 2; - const uint64_t m = n / halfnn; - - // const uint64_t hpow = UINT64_C(1) << level_metadata_ptr->half_bs; - for (uint64_t i = 1; i < halfnn; ++i) { - uint64_t t = powomega[i * m]; - uint64_t t1 = (t << level_metadata_ptr->half_bs) % q; - powomega_ptr[4 * (i - 1)] = (t1 << 32) + t; - } - powomega_ptr += 4 * (halfnn - 1); - level_metadata_ptr++; - } - } - free(powomega); - - return precomp; -} - -EXPORT q120_ntt_precomp* q120_new_intt_bb_precomp(const uint64_t n) { - uint32_t omega_vec[4]; - fill_omegas(n, omega_vec); - - const uint64_t logQ = ceil(log2(PRIMES_VEC[0])); - - q120_ntt_precomp* precomp = new_precomp(n); - - uint64_t bs = precomp->input_bit_size = 64; - - LOG("iNTT parameters:\n"); - LOG("\tsize = %" PRIu64 "\n", n) - LOG("\tlogQ = %" PRIu64 "\n", logQ); - LOG("\tinput bit-size = %" PRIu64 "\n", bs); - - if (n == 1) return precomp; - - // fill reduction metadata - uint64_t bs_after_reduc = fill_reduction_meta(bs, &(precomp->reduc_metadata)); - - // backward metadata - q120_ntt_step_precomp* level_metadata_ptr = precomp->level_metadata; - - // first level (a+b, a-b) adds 1-bit - { - level_metadata_ptr->reduce = (bs == 64); - if (level_metadata_ptr->reduce) { - bs = bs_after_reduc; - LOG("\treduce output bit-size = %" PRIu64 "\n", bs); - } - - for (int k = 0; k < 4; ++k) level_metadata_ptr->q2bs[k] = (uint64_t)PRIMES_VEC[k] << (bs - logQ); - - level_metadata_ptr->bs = ++bs; - level_metadata_ptr->half_bs = level_metadata_ptr->mask = UINT64_C(0); // not used - LOG("\tlevel %6" PRIu64 " output bit-size = %" PRIu64 "\n", UINT64_C(1), bs); - level_metadata_ptr++; - } - - for (uint64_t nn = 4; nn <= n; nn *= 2) { - level_metadata_ptr->reduce = (bs == 64); - if (level_metadata_ptr->reduce) { - bs = bs_after_reduc; - LOG("\treduce output bit-size = %" PRIu64 "\n", bs); - } - - const uint64_t half_bs = round_up_half_n(bs); - const uint64_t bs_mult = half_bs + logQ + 1; // bit-size of term b.omega^k - bs = 1 + ((bs > bs_mult) ? bs : bs_mult); // bit-size of a+b.omega^k or a-b.omega^k - assert(bs <= 64); - - level_metadata_ptr->bs = bs; - level_metadata_ptr->half_bs = half_bs; - level_metadata_ptr->mask = (UINT64_C(1) << half_bs) - UINT64_C(1); - for (int k = 0; k < 4; ++k) level_metadata_ptr->q2bs[k] = (uint64_t)PRIMES_VEC[k] << (bs_mult - logQ); - level_metadata_ptr++; - - LOG("\tlevel %6" PRIu64 " output bit-size = %" PRIu64 "\n", nn / 2, bs); - } - - // last level a_k.omega^k - { - level_metadata_ptr->reduce = (bs == 64); - if (level_metadata_ptr->reduce) { - bs = bs_after_reduc; - LOG("\treduce output bit-size = %" PRIu64 "\n", bs); - } - - const uint64_t half_bs = round_up_half_n(bs); - - bs = half_bs + logQ + 1; // bit-size of term a.omega^k - assert(bs <= 64); - - level_metadata_ptr->bs = bs; - level_metadata_ptr->half_bs = half_bs; - level_metadata_ptr->mask = (UINT64_C(1) << half_bs) - UINT64_C(1); - for (int k = 0; k < 4; ++k) level_metadata_ptr->q2bs[k] = (uint64_t)PRIMES_VEC[k] << (bs - logQ); - - LOG("\tlevel %6" PRIu64 " output bit-size = %" PRIu64 "\n", n, bs); - } - - // omega powers - uint32_t* powomegabar = malloc(sizeof(*powomegabar) * 2 * n); - for (int k = 0; k < 4; ++k) { - const uint64_t q = PRIMES_VEC[k]; - - for (uint64_t i = 0; i < 2 * n; ++i) { - powomegabar[i] = modq_pow(omega_vec[k], -i, q); - } - - uint64_t* powomega_ptr = precomp->powomega + k; - level_metadata_ptr = precomp->level_metadata + 1; - - for (uint64_t nn = 4; nn <= n; nn *= 2) { - const uint64_t halfnn = nn / 2; - const uint64_t m = n / halfnn; - - for (uint64_t i = 1; i < halfnn; ++i) { - uint64_t t = powomegabar[i * m]; - uint64_t t1 = (t << level_metadata_ptr->half_bs) % q; - powomega_ptr[4 * (i - 1)] = (t1 << 32) + t; - } - powomega_ptr += 4 * (halfnn - 1); - level_metadata_ptr++; - } - - { - const uint64_t invNmod = modq_pow(n, -1, q); - for (uint64_t i = 0; i < n; ++i) { - uint64_t t = (powomegabar[i] * invNmod) % q; - uint64_t t1 = (t << level_metadata_ptr->half_bs) % q; - powomega_ptr[4 * i] = (t1 << 32) + t; - } - } - } - - free(powomegabar); - - return precomp; -} - -void del_precomp(q120_ntt_precomp* precomp) { - spqlios_free(precomp->powomega); - free(precomp->level_metadata); - free(precomp); -} - -EXPORT void q120_del_ntt_bb_precomp(q120_ntt_precomp* precomp) { del_precomp(precomp); } - -EXPORT void q120_del_intt_bb_precomp(q120_ntt_precomp* precomp) { del_precomp(precomp); }
@@ -1,25 +0,0 @@
-#ifndef SPQLIOS_Q120_NTT_H -#define SPQLIOS_Q120_NTT_H - -#include "../commons.h" -#include "q120_common.h" - -typedef struct _q120_ntt_precomp q120_ntt_precomp; - -EXPORT q120_ntt_precomp* q120_new_ntt_bb_precomp(const uint64_t n); -EXPORT void q120_del_ntt_bb_precomp(q120_ntt_precomp* precomp); - -EXPORT q120_ntt_precomp* q120_new_intt_bb_precomp(const uint64_t n); -EXPORT void q120_del_intt_bb_precomp(q120_ntt_precomp* precomp); - -/** - * @brief computes a direct ntt in-place over data. - */ -EXPORT void q120_ntt_bb_avx2(const q120_ntt_precomp* const precomp, q120b* const data); - -/** - * @brief computes an inverse ntt in-place over data. - */ -EXPORT void q120_intt_bb_avx2(const q120_ntt_precomp* const precomp, q120b* const data); - -#endif // SPQLIOS_Q120_NTT_H
@@ -1,479 +0,0 @@
-#include <assert.h> -#include <immintrin.h> -#include <inttypes.h> -#include <math.h> -#include <stdint.h> -#include <stdio.h> - -#include "q120_common.h" -#include "q120_ntt_private.h" - -// at which level to switch from computations by level to computations by block -#define CHANGE_MODE_N 1024 - -__always_inline __m256i split_precompmul_si256(__m256i inp, __m256i powomega, const uint64_t h, const __m256i mask) { - const __m256i inp_low = _mm256_and_si256(inp, mask); - const __m256i t1 = _mm256_mul_epu32(inp_low, powomega); - - const __m256i inp_high = _mm256_srli_epi64(inp, h); - const __m256i powomega_high = _mm256_srli_epi64(powomega, 32); - const __m256i t2 = _mm256_mul_epu32(inp_high, powomega_high); - - return _mm256_add_epi64(t1, t2); -} - -__always_inline __m256i modq_red(const __m256i x, const uint64_t h, const __m256i mask, const __m256i _2_pow_h_modq) { - __m256i xh = _mm256_srli_epi64(x, h); - __m256i xl = _mm256_and_si256(x, mask); - __m256i xh_1 = _mm256_mul_epu32(xh, _2_pow_h_modq); - return _mm256_add_epi64(xl, xh_1); -} - -void print_data(const uint64_t n, const uint64_t* const data, const uint64_t q) { - for (uint64_t i = 0; i < n; i++) { - printf("%" PRIu64 " ", *(data + i) % q); - } - printf("\n"); -} - -double max_bit_size(const void* const begin, const void* const end) { - double bs = 0; - const uint64_t* data = (uint64_t*)begin; - while (data != end) { - double t = log2(*(data++)); - if (bs < t) { - bs = t; - } - } - return bs; -} - -void ntt_iter_first(__m256i* const begin, const __m256i* const end, const q120_ntt_step_precomp* const itData, - const __m256i* powomega) { - const uint64_t h = itData->half_bs; - const __m256i vmask = _mm256_set1_epi64x(itData->mask); - - __m256i* data = begin; - while (data < end) { - __m256i x = _mm256_loadu_si256(data); - __m256i po = _mm256_loadu_si256(powomega); - __m256i r = split_precompmul_si256(x, po, h, vmask); - _mm256_storeu_si256(data, r); - - data++; - powomega++; - } -} - -void ntt_iter(const uint64_t nn, __m256i* const begin, const __m256i* const end, - const q120_ntt_step_precomp* const itData, const __m256i* const powomega) { - assert(nn % 2 == 0); - const uint64_t halfnn = nn / 2; - - const __m256i vq2bs = _mm256_loadu_si256((__m256i*)itData->q2bs); - const __m256i vmask = _mm256_set1_epi64x(itData->mask); - - __m256i* data = begin; - while (data < end) { - __m256i* ptr1 = data; - __m256i* ptr2 = data + halfnn; - - const __m256i a = _mm256_loadu_si256(ptr1); - const __m256i b = _mm256_loadu_si256(ptr2); - - const __m256i ap = _mm256_add_epi64(a, b); - _mm256_storeu_si256(ptr1, ap); - - const __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), b); - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - - const __m256i* po_ptr = powomega; - for (uint64_t i = 1; i < halfnn; ++i) { - __m256i a = _mm256_loadu_si256(ptr1); - __m256i b = _mm256_loadu_si256(ptr2); - - __m256i ap = _mm256_add_epi64(a, b); - - _mm256_storeu_si256(ptr1, ap); - - __m256i b1 = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), b); - __m256i po = _mm256_loadu_si256(po_ptr); - - __m256i bp = split_precompmul_si256(b1, po, itData->half_bs, vmask); - - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - po_ptr++; - } - data += nn; - } -} - -void ntt_iter_red(const uint64_t nn, __m256i* const begin, const __m256i* const end, - const q120_ntt_step_precomp* const itData, const __m256i* const powomega, - const q120_ntt_reduc_step_precomp* const reduc_precomp) { - assert(nn % 2 == 0); - const uint64_t halfnn = nn / 2; - - const __m256i vq2bs = _mm256_loadu_si256((__m256i*)itData->q2bs); - const __m256i vmask = _mm256_set1_epi64x(itData->mask); - - const __m256i reduc_mask = _mm256_set1_epi64x(reduc_precomp->mask); - const __m256i reduc_cst = _mm256_loadu_si256((__m256i*)reduc_precomp->modulo_red_cst); - - __m256i* data = begin; - while (data < end) { - __m256i* ptr1 = data; - __m256i* ptr2 = data + halfnn; - - __m256i a = _mm256_loadu_si256(ptr1); - __m256i b = _mm256_loadu_si256(ptr2); - - a = modq_red(a, reduc_precomp->h, reduc_mask, reduc_cst); - b = modq_red(b, reduc_precomp->h, reduc_mask, reduc_cst); - - const __m256i ap = _mm256_add_epi64(a, b); - _mm256_storeu_si256(ptr1, ap); - - const __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), b); - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - - const __m256i* po_ptr = powomega; - for (uint64_t i = 1; i < halfnn; ++i) { - __m256i a = _mm256_loadu_si256(ptr1); - __m256i b = _mm256_loadu_si256(ptr2); - - a = modq_red(a, reduc_precomp->h, reduc_mask, reduc_cst); - b = modq_red(b, reduc_precomp->h, reduc_mask, reduc_cst); - - __m256i ap = _mm256_add_epi64(a, b); - - _mm256_storeu_si256(ptr1, ap); - - __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), b); - __m256i po = _mm256_loadu_si256(po_ptr); - bp = split_precompmul_si256(bp, po, itData->half_bs, vmask); - - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - po_ptr++; - } - data += nn; - } -} - -EXPORT void q120_ntt_bb_avx2(const q120_ntt_precomp* const precomp, q120b* const data_ptr) { - // assert((size_t)data_ptr % 32 == 0); // alignment check - - const uint64_t n = precomp->n; - if (n == 1) return; - - const q120_ntt_step_precomp* itData = precomp->level_metadata; - const __m256i* powomega = (__m256i*)precomp->powomega; - - __m256i* const begin = (__m256i*)data_ptr; - const __m256i* const end = ((__m256i*)data_ptr) + n; - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Input %lf %" PRIu64 "\n", bs, precomp->input_bit_size); - assert(bs <= precomp->input_bit_size); - } - - // first iteration a_k.omega^k - ntt_iter_first(begin, end, itData, powomega); - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Iter %3" PRIu64 " - %lf %" PRIu64 "\n", n, bs, itData->bs); - assert(bs < itData->bs); - } - - powomega += n; - itData++; - - const uint64_t split_nn = (CHANGE_MODE_N > n) ? n : CHANGE_MODE_N; - // const uint64_t split_nn = 2; - - // computations by level - for (uint64_t nn = n; nn > split_nn; nn /= 2) { - const uint64_t halfnn = nn / 2; - - if (itData->reduce) { - ntt_iter_red(nn, begin, end, itData, powomega, &precomp->reduc_metadata); - } else { - ntt_iter(nn, begin, end, itData, powomega); - } - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Iter %3" PRIu64 " - %lf %" PRIu64 " %c\n", nn / 2, bs, itData->bs, itData->reduce ? '*' : ' '); - assert(bs < itData->bs); - } - - powomega += halfnn - 1; - itData++; - } - - // computations by memory block - if (split_nn >= 2) { - const q120_ntt_step_precomp* itData1 = itData; - const __m256i* powomega1 = powomega; - for (__m256i* it = begin; it < end; it += split_nn) { - __m256i* const begin1 = it; - const __m256i* const end1 = it + split_nn; - - itData = itData1; - powomega = powomega1; - for (uint64_t nn = split_nn; nn >= 2; nn /= 2) { - const uint64_t halfnn = nn / 2; - - if (itData->reduce) { - ntt_iter_red(nn, begin1, end1, itData, powomega, &precomp->reduc_metadata); - } else { - ntt_iter(nn, begin1, end1, itData, powomega); - } - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((uint64_t*)begin1, (uint64_t*)end1); - // LOG("Iter %3lu - %lf %lu\n", nn / 2, bs, itData->bs); - assert(bs < itData->bs); - } - - powomega += halfnn - 1; - itData++; - } - } - } - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Iter %3" PRIu64 " - %lf %" PRIu64 "\n", UINT64_C(1), bs, precomp->output_bit_size); - assert(bs < precomp->output_bit_size); - } -} - -void intt_iter(const uint64_t nn, __m256i* const begin, const __m256i* const end, - const q120_ntt_step_precomp* const itData, const __m256i* const powomega) { - assert(nn % 2 == 0); - const uint64_t halfnn = nn / 2; - - const __m256i vq2bs = _mm256_loadu_si256((__m256i*)itData->q2bs); - const __m256i vmask = _mm256_set1_epi64x(itData->mask); - - __m256i* data = begin; - while (data < end) { - __m256i* ptr1 = data; - __m256i* ptr2 = data + halfnn; - - const __m256i a = _mm256_loadu_si256(ptr1); - const __m256i b = _mm256_loadu_si256(ptr2); - - const __m256i ap = _mm256_add_epi64(a, b); - _mm256_storeu_si256(ptr1, ap); - - const __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), b); - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - - const __m256i* po_ptr = powomega; - for (uint64_t i = 1; i < halfnn; ++i) { - __m256i a = _mm256_loadu_si256(ptr1); - __m256i b = _mm256_loadu_si256(ptr2); - - __m256i po = _mm256_loadu_si256(po_ptr); - __m256i bo = split_precompmul_si256(b, po, itData->half_bs, vmask); - - __m256i ap = _mm256_add_epi64(a, bo); - - _mm256_storeu_si256(ptr1, ap); - - __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), bo); - - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - po_ptr++; - } - data += nn; - } -} - -void intt_iter_red(const uint64_t nn, __m256i* const begin, const __m256i* const end, - const q120_ntt_step_precomp* const itData, const __m256i* const powomega, - const q120_ntt_reduc_step_precomp* const reduc_precomp) { - assert(nn % 2 == 0); - const uint64_t halfnn = nn / 2; - - const __m256i vq2bs = _mm256_loadu_si256((__m256i*)itData->q2bs); - const __m256i vmask = _mm256_set1_epi64x(itData->mask); - - const __m256i reduc_mask = _mm256_set1_epi64x(reduc_precomp->mask); - const __m256i reduc_cst = _mm256_loadu_si256((__m256i*)reduc_precomp->modulo_red_cst); - - __m256i* data = begin; - while (data < end) { - __m256i* ptr1 = data; - __m256i* ptr2 = data + halfnn; - - __m256i a = _mm256_loadu_si256(ptr1); - __m256i b = _mm256_loadu_si256(ptr2); - - a = modq_red(a, reduc_precomp->h, reduc_mask, reduc_cst); - b = modq_red(b, reduc_precomp->h, reduc_mask, reduc_cst); - - const __m256i ap = _mm256_add_epi64(a, b); - _mm256_storeu_si256(ptr1, ap); - - const __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), b); - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - - const __m256i* po_ptr = powomega; - for (uint64_t i = 1; i < halfnn; ++i) { - __m256i a = _mm256_loadu_si256(ptr1); - __m256i b = _mm256_loadu_si256(ptr2); - - a = modq_red(a, reduc_precomp->h, reduc_mask, reduc_cst); - b = modq_red(b, reduc_precomp->h, reduc_mask, reduc_cst); - - __m256i po = _mm256_loadu_si256(po_ptr); - __m256i bo = split_precompmul_si256(b, po, itData->half_bs, vmask); - - __m256i ap = _mm256_add_epi64(a, bo); - - _mm256_storeu_si256(ptr1, ap); - - __m256i bp = _mm256_sub_epi64(_mm256_add_epi64(a, vq2bs), bo); - - _mm256_storeu_si256(ptr2, bp); - - ptr1++; - ptr2++; - po_ptr++; - } - data += nn; - } -} - -void ntt_iter_first_red(__m256i* const begin, const __m256i* const end, const q120_ntt_step_precomp* const itData, - const __m256i* powomega, const q120_ntt_reduc_step_precomp* const reduc_precomp) { - const uint64_t h = itData->half_bs; - const __m256i vmask = _mm256_set1_epi64x(itData->mask); - - const __m256i reduc_mask = _mm256_set1_epi64x(reduc_precomp->mask); - const __m256i reduc_cst = _mm256_loadu_si256((__m256i*)reduc_precomp->modulo_red_cst); - - __m256i* data = begin; - while (data < end) { - __m256i x = _mm256_loadu_si256(data); - x = modq_red(x, reduc_precomp->h, reduc_mask, reduc_cst); - __m256i po = _mm256_loadu_si256(powomega); - __m256i r = split_precompmul_si256(x, po, h, vmask); - _mm256_storeu_si256(data, r); - - data++; - powomega++; - } -} - -EXPORT void q120_intt_bb_avx2(const q120_ntt_precomp* const precomp, q120b* const data_ptr) { - // assert((size_t)data_ptr % 32 == 0); // alignment check - - const uint64_t n = precomp->n; - if (n == 1) return; - - const q120_ntt_step_precomp* itData = precomp->level_metadata; - const __m256i* powomega = (__m256i*)precomp->powomega; - - __m256i* const begin = (__m256i*)data_ptr; - const __m256i* const end = ((__m256i*)data_ptr) + n; - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Input %lf %" PRIu64 "\n", bs, precomp->input_bit_size); - assert(bs <= precomp->input_bit_size); - } - - const uint64_t split_nn = (CHANGE_MODE_N > n) ? n : CHANGE_MODE_N; - - // computations by memory block - if (split_nn >= 2) { - const q120_ntt_step_precomp* itData1 = itData; - const __m256i* powomega1 = powomega; - for (__m256i* it = begin; it < end; it += split_nn) { - __m256i* const begin1 = it; - const __m256i* const end1 = it + split_nn; - - itData = itData1; - powomega = powomega1; - for (uint64_t nn = 2; nn <= split_nn; nn *= 2) { - const uint64_t halfnn = nn / 2; - - if (itData->reduce) { - intt_iter_red(nn, begin1, end1, itData, powomega, &precomp->reduc_metadata); - } else { - intt_iter(nn, begin1, end1, itData, powomega); - } - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((uint64_t*)begin1, (uint64_t*)end1); - // LOG("Iter %3lu - %lf %lu\n", nn / 2, bs, itData->bs); - assert(bs < itData->bs); - } - - powomega += halfnn - 1; - itData++; - } - } - } - - // computations by level - // for (uint64_t nn = 2; nn <= n; nn *= 2) { - for (uint64_t nn = 2 * split_nn; nn <= n; nn *= 2) { - const uint64_t halfnn = nn / 2; - - if (itData->reduce) { - intt_iter_red(nn, begin, end, itData, powomega, &precomp->reduc_metadata); - } else { - intt_iter(nn, begin, end, itData, powomega); - } - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Iter %3" PRIu64 " - %lf %" PRIu64 " %c\n", nn / 2, bs, itData->bs, itData->reduce ? '*' : ' '); - assert(bs < itData->bs); - } - - powomega += halfnn - 1; - itData++; - } - - // last iteration a_k . omega^k . n^-1 - if (itData->reduce) { - ntt_iter_first_red(begin, end, itData, powomega, &precomp->reduc_metadata); - } else { - ntt_iter_first(begin, end, itData, powomega); - } - - if (CHECK_BOUNDS) { - double bs __attribute__((unused)) = max_bit_size((void*)begin, (void*)end); - LOG("Iter %3" PRIu64 " - %lf %" PRIu64 "\n", n, bs, itData->bs); - assert(bs < itData->bs); - } -}
@@ -1,39 +0,0 @@
-#include "q120_ntt.h" - -#ifndef NDEBUG -#define CHECK_BOUNDS 1 -#define VERBOSE -#else -#define CHECK_BOUNDS 0 -#endif - -#ifndef VERBOSE -#define LOG(...) ; -#else -#define LOG(...) printf(__VA_ARGS__); -#endif - -typedef struct _q120_ntt_step_precomp { - uint64_t q2bs[4]; // q2bs = 2^{bs-31}.q[k] - uint64_t bs; // inputs at this iterations must be in Q_n - uint64_t half_bs; // == ceil(bs/2) - uint64_t mask; // (1<<half_bs) - 1 - uint8_t reduce; -} q120_ntt_step_precomp; - -typedef struct _q120_ntt_reduc_step_precomp { - uint64_t modulo_red_cst[4]; - uint64_t mask; - uint64_t h; -} q120_ntt_reduc_step_precomp; - -typedef struct _q120_ntt_precomp { - uint64_t n; // NTT size (a power of 2) - - q120_ntt_step_precomp* level_metadata; - uint64_t* powomega; - q120_ntt_reduc_step_precomp reduc_metadata; - - uint64_t input_bit_size; - uint64_t output_bit_size; -} q120_ntt_precomp;
@@ -1,212 +0,0 @@
-#include <errno.h> -#include <string.h> - -#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -void reim_from_znx64_ref(const REIM_FROM_ZNX64_PRECOMP* precomp, void* r, const int64_t* x) { - // naive version of the function (just cast) - const uint64_t nn = precomp->m << 1; - double* res = (double*)r; - for (uint64_t i = 0; i < nn; ++i) { - res[i] = (double)x[i]; - } -} - -void* init_reim_from_znx64_precomp(REIM_FROM_ZNX64_PRECOMP* const res, uint32_t m, uint32_t log2bound) { - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - // currently, we are going to use the trick add 3.2^51, mask the exponent, reinterpret bits as double. - // therefore we need the input values to be < 2^50. - if (log2bound > 50) return spqlios_error("Invalid log2bound error: must be in [0,50]"); - res->m = m; - FROM_ZNX64_FUNC resf = reim_from_znx64_ref; - if (m >= 8) { - if (CPU_SUPPORTS("avx2")) { - resf = reim_from_znx64_bnd50_fma; - } - } - res->function = resf; - return res; -} - -EXPORT REIM_FROM_ZNX64_PRECOMP* new_reim_from_znx64_precomp(uint32_t m, uint32_t log2bound) { - REIM_FROM_ZNX64_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim_from_znx64_precomp(res, m, log2bound)); -} - -EXPORT void reim_from_znx64(const REIM_FROM_ZNX64_PRECOMP* tables, void* r, const int64_t* a) { - tables->function(tables, r, a); -} - -/** - * @brief Simpler API for the znx64 to reim conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment - */ -EXPORT void reim_from_znx64_simple(uint32_t m, uint32_t log2bound, void* r, const int64_t* a) { - // not checking for log2bound which is not relevant here - static REIM_FROM_ZNX64_PRECOMP precomp[32]; - REIM_FROM_ZNX64_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_reim_from_znx64_precomp(p, m, log2bound)) abort(); - } - p->function(p, r, a); -} - -void reim_from_znx32_ref(const REIM_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x) { NOT_IMPLEMENTED(); } -void reim_from_znx32_avx2_fma(const REIM_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x) { NOT_IMPLEMENTED(); } - -void* init_reim_from_znx32_precomp(REIM_FROM_ZNX32_PRECOMP* const res, uint32_t m, uint32_t log2bound) { - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - if (log2bound > 32) return spqlios_error("Invalid log2bound error: must be in [0,32]"); - res->m = m; - // TODO: check selection logic - if (CPU_SUPPORTS("avx2")) { - if (m >= 8) { - res->function = reim_from_znx32_avx2_fma; - } else { - res->function = reim_from_znx32_ref; - } - } else { - res->function = reim_from_znx32_ref; - } - return res; -} - -EXPORT REIM_FROM_ZNX32_PRECOMP* new_reim_from_znx32_precomp(uint32_t m, uint32_t log2bound) { - REIM_FROM_ZNX32_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim_from_znx32_precomp(res, m, log2bound)); -} - -void reim_from_tnx32_ref(const REIM_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x) { NOT_IMPLEMENTED(); } -void reim_from_tnx32_avx2_fma(const REIM_FROM_TNX32_PRECOMP* precomp, void* r, const int32_t* x) { NOT_IMPLEMENTED(); } - -void* init_reim_from_tnx32_precomp(REIM_FROM_TNX32_PRECOMP* const res, uint32_t m) { - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - res->m = m; - // TODO: check selection logic - if (CPU_SUPPORTS("avx2")) { - if (m >= 8) { - res->function = reim_from_tnx32_avx2_fma; - } else { - res->function = reim_from_tnx32_ref; - } - } else { - res->function = reim_from_tnx32_ref; - } - return res; -} - -EXPORT REIM_FROM_TNX32_PRECOMP* new_reim_from_tnx32_precomp(uint32_t m) { - REIM_FROM_TNX32_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim_from_tnx32_precomp(res, m)); -} - -void reim_to_tnx32_ref(const REIM_TO_TNX32_PRECOMP* precomp, int32_t* r, const void* x) { NOT_IMPLEMENTED(); } -void reim_to_tnx32_avx2_fma(const REIM_TO_TNX32_PRECOMP* precomp, int32_t* r, const void* x) { NOT_IMPLEMENTED(); } - -void* init_reim_to_tnx32_precomp(REIM_TO_TNX32_PRECOMP* const res, uint32_t m, double divisor, uint32_t log2overhead) { - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - if (is_not_pow2_double(&divisor)) return spqlios_error("divisor must be a power of 2"); - if (log2overhead > 52) return spqlios_error("log2overhead is too large"); - res->m = m; - res->divisor = divisor; - // TODO: check selection logic - if (CPU_SUPPORTS("avx2")) { - if (log2overhead <= 18) { - if (m >= 8) { - res->function = reim_to_tnx32_avx2_fma; - } else { - res->function = reim_to_tnx32_ref; - } - } else { - res->function = reim_to_tnx32_ref; - } - } else { - res->function = reim_to_tnx32_ref; - } - return res; -} - -EXPORT REIM_TO_TNX32_PRECOMP* new_reim_to_tnx32_precomp(uint32_t m, double divisor, uint32_t log2overhead) { - REIM_TO_TNX32_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim_to_tnx32_precomp(res, m, divisor, log2overhead)); -} - -EXPORT void reim_from_znx32_simple(uint32_t m, uint32_t log2bound, void* r, const int32_t* x) { - static REIM_FROM_ZNX32_PRECOMP* p[31] = {0}; - REIM_FROM_ZNX32_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_from_znx32_precomp(m, log2bound); - (*f)->function(*f, r, x); -} - -EXPORT void reim_from_tnx32_simple(uint32_t m, void* r, const int32_t* x) { - static REIM_FROM_TNX32_PRECOMP* p[31] = {0}; - REIM_FROM_TNX32_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_from_tnx32_precomp(m); - (*f)->function(*f, r, x); -} - -EXPORT void reim_to_tnx32_simple(uint32_t m, double divisor, uint32_t log2overhead, int32_t* r, const void* x) { - static REIM_TO_TNX32_PRECOMP* p[31] = {0}; - REIM_TO_TNX32_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_to_tnx32_precomp(m, divisor, log2overhead); - (*f)->function(*f, r, x); -} - -void reim_to_znx64_ref(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* x) { - // for now, we stick to a slow implem - uint64_t nn = precomp->m << 1; - const double* v = (double*)x; - double invdiv = 1. / precomp->divisor; - for (uint64_t i = 0; i < nn; ++i) { - r[i] = (int64_t)rint(v[i] * invdiv); - } -} - -void* init_reim_to_znx64_precomp(REIM_TO_ZNX64_PRECOMP* const res, uint32_t m, double divisor, uint32_t log2bound) { - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - if (is_not_pow2_double(&divisor)) return spqlios_error("divisor must be a power of 2"); - if (log2bound > 64) return spqlios_error("log2bound is too large"); - res->m = m; - res->divisor = divisor; - TO_ZNX64_FUNC resf = reim_to_znx64_ref; - if (CPU_SUPPORTS("avx2") && m >= 8) { - if (log2bound <= 50) { - resf = reim_to_znx64_avx2_bnd50_fma; - } else { - resf = reim_to_znx64_avx2_bnd63_fma; - } - } - res->function = resf; // must be the last one set - return res; -} - -EXPORT REIM_TO_ZNX64_PRECOMP* new_reim_to_znx64_precomp(uint32_t m, double divisor, uint32_t log2bound) { - REIM_TO_ZNX64_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim_to_znx64_precomp(res, m, divisor, log2bound)); -} - -EXPORT void reim_to_znx64(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* a) { - precomp->function(precomp, r, a); -} - -/** - * @brief Simpler API for the znx64 to reim conversion. - */ -EXPORT void reim_to_znx64_simple(uint32_t m, double divisor, uint32_t log2bound, int64_t* r, const void* a) { - // not checking distinguishing <=50 or not - static __thread REIM_TO_ZNX64_PRECOMP p; - static __thread uint32_t prev_log2bound; - if (!p.function || p.m != m || p.divisor != divisor || prev_log2bound != log2bound) { - if (!init_reim_to_znx64_precomp(&p, m, divisor, log2bound)) abort(); - prev_log2bound = log2bound; - } - p.function(&p, r, a); -}
@@ -1,106 +0,0 @@
-#include <immintrin.h> - -#include "reim_fft_private.h" - -void reim_from_znx64_bnd50_fma(const REIM_FROM_ZNX64_PRECOMP* precomp, void* r, const int64_t* x) { - static const double EXPO = INT64_C(1) << 52; - const int64_t ADD_CST = INT64_C(1) << 51; - const double SUB_CST = INT64_C(3) << 51; - - const __m256d SUB_CST_4 = _mm256_set1_pd(SUB_CST); - const __m256i ADD_CST_4 = _mm256_set1_epi64x(ADD_CST); - const __m256d EXPO_4 = _mm256_set1_pd(EXPO); - - double(*out)[4] = (double(*)[4])r; - __m256i* in = (__m256i*)x; - __m256i* inend = (__m256i*)(x + (precomp->m << 1)); - do { - // read the next value - __m256i a = _mm256_loadu_si256(in); - a = _mm256_add_epi64(a, ADD_CST_4); - __m256d ad = _mm256_castsi256_pd(a); - ad = _mm256_or_pd(ad, EXPO_4); - ad = _mm256_sub_pd(ad, SUB_CST_4); - // store the next value - _mm256_storeu_pd(out[0], ad); - ++out; - ++in; - } while (in < inend); -} - -// version where the output norm can be as big as 2^63 -void reim_to_znx64_avx2_bnd63_fma(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* x) { - static const uint64_t SIGN_MASK = 0x8000000000000000UL; - static const uint64_t EXPO_MASK = 0x7FF0000000000000UL; - static const uint64_t MANTISSA_MASK = 0x000FFFFFFFFFFFFFUL; - static const uint64_t MANTISSA_MSB = 0x0010000000000000UL; - const double divisor_bits = precomp->divisor * ((double)(INT64_C(1) << 52)); - const double offset = precomp->divisor / 2.; - - const __m256d SIGN_MASK_4 = _mm256_castsi256_pd(_mm256_set1_epi64x(SIGN_MASK)); - const __m256i EXPO_MASK_4 = _mm256_set1_epi64x(EXPO_MASK); - const __m256i MANTISSA_MASK_4 = _mm256_set1_epi64x(MANTISSA_MASK); - const __m256i MANTISSA_MSB_4 = _mm256_set1_epi64x(MANTISSA_MSB); - const __m256d offset_4 = _mm256_set1_pd(offset); - const __m256i divi_bits_4 = _mm256_castpd_si256(_mm256_set1_pd(divisor_bits)); - - double(*in)[4] = (double(*)[4])x; - __m256i* out = (__m256i*)r; - __m256i* outend = (__m256i*)(r + (precomp->m << 1)); - do { - // read the next value - __m256d a = _mm256_loadu_pd(in[0]); - // a += sign(a) * m/2 - __m256d asign = _mm256_and_pd(a, SIGN_MASK_4); - a = _mm256_add_pd(a, _mm256_or_pd(asign, offset_4)); - // sign: either 0 or -1 - __m256i sign_mask = _mm256_castpd_si256(asign); - sign_mask = _mm256_sub_epi64(_mm256_set1_epi64x(0), _mm256_srli_epi64(sign_mask, 63)); - // compute the exponents - __m256i a0exp = _mm256_and_si256(_mm256_castpd_si256(a), EXPO_MASK_4); - __m256i a0lsh = _mm256_sub_epi64(a0exp, divi_bits_4); - __m256i a0rsh = _mm256_sub_epi64(divi_bits_4, a0exp); - a0lsh = _mm256_srli_epi64(a0lsh, 52); - a0rsh = _mm256_srli_epi64(a0rsh, 52); - // compute the new mantissa - __m256i a0pos = _mm256_and_si256(_mm256_castpd_si256(a), MANTISSA_MASK_4); - a0pos = _mm256_or_si256(a0pos, MANTISSA_MSB_4); - a0lsh = _mm256_sllv_epi64(a0pos, a0lsh); - a0rsh = _mm256_srlv_epi64(a0pos, a0rsh); - __m256i final = _mm256_or_si256(a0lsh, a0rsh); - // negate if the sign was negative - final = _mm256_xor_si256(final, sign_mask); - final = _mm256_sub_epi64(final, sign_mask); - // read the next value - _mm256_storeu_si256(out, final); - ++out; - ++in; - } while (out < outend); -} - -// version where the output norm can be as big as 2^50 -void reim_to_znx64_avx2_bnd50_fma(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* x) { - static const uint64_t MANTISSA_MASK = 0x000FFFFFFFFFFFFFUL; - const int64_t SUB_CST = INT64_C(1) << 51; - const double add_cst = precomp->divisor * ((double)(INT64_C(3) << 51)); - - const __m256i SUB_CST_4 = _mm256_set1_epi64x(SUB_CST); - const __m256d add_cst_4 = _mm256_set1_pd(add_cst); - const __m256i MANTISSA_MASK_4 = _mm256_set1_epi64x(MANTISSA_MASK); - - double(*in)[4] = (double(*)[4])x; - __m256i* out = (__m256i*)r; - __m256i* outend = (__m256i*)(r + (precomp->m << 1)); - do { - // read the next value - __m256d a = _mm256_loadu_pd(in[0]); - a = _mm256_add_pd(a, add_cst_4); - __m256i ai = _mm256_castpd_si256(a); - ai = _mm256_and_si256(ai, MANTISSA_MASK_4); - ai = _mm256_sub_epi64(ai, SUB_CST_4); - // store the next value - _mm256_storeu_si256(out, ai); - ++out; - ++in; - } while (out < outend); -}
@@ -1,44 +0,0 @@
-#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -EXPORT void reim_from_znx32(const REIM_FROM_ZNX32_PRECOMP* tables, void* r, const int32_t* a) { - tables->function(tables, r, a); -} - -EXPORT void reim_from_tnx32(const REIM_FROM_TNX32_PRECOMP* tables, void* r, const int32_t* a) { - tables->function(tables, r, a); -} - -EXPORT void reim_to_tnx32(const REIM_TO_TNX32_PRECOMP* tables, int32_t* r, const void* a) { - tables->function(tables, r, a); -} - -EXPORT void reim_fftvec_add(const REIM_FFTVEC_ADD_PRECOMP* tables, double* r, const double* a, const double* b) { - tables->function(tables, r, a, b); -} - -EXPORT void reim_fftvec_sub(const REIM_FFTVEC_SUB_PRECOMP* tables, double* r, const double* a, const double* b) { - tables->function(tables, r, a, b); -} - -EXPORT void reim_fftvec_mul(const REIM_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b) { - tables->function(tables, r, a, b); -} - -EXPORT void reim_fftvec_addmul(const REIM_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, const double* b) { - tables->function(tables, r, a, b); -} - -EXPORT void reim_fftvec_automorphism(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* tables, int64_t p, double* r, - const double* a, uint64_t a_size) { - tables->function.apply(tables, p, r, a, a_size); -} - -EXPORT void reim_fftvec_automorphism_inplace(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* tables, int64_t p, double* a, - uint64_t a_size, uint8_t* tmp_bytes) { - tables->function.apply_inplace(tables, p, a, a_size, tmp_bytes); -} - -EXPORT uint64_t reim_fftvec_automorphism_inplace_tmp_bytes(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* tables) { - return tables->function.apply_inplace_tmp_bytes(tables); -}
@@ -1,21 +0,0 @@
-#include "reim_fft_private.h" - -EXPORT void reim_fftvec_addmul_fma(const REIM_FFTVEC_ADDMUL_PRECOMP* precomp, double* r, const double* a, - const double* b) { - UNDEFINED(); -} -EXPORT void reim_fftvec_mul_fma(const REIM_FFTVEC_MUL_PRECOMP* precomp, double* r, const double* a, const double* b) { - UNDEFINED(); -} -EXPORT void reim_fftvec_add_fma(const REIM_FFTVEC_ADD_PRECOMP* precomp, double* r, const double* a, const double* b) { - UNDEFINED(); -} -EXPORT void reim_fftvec_sub_fma(const REIM_FFTVEC_SUB_PRECOMP* precomp, double* r, const double* a, const double* b) { - UNDEFINED(); -} - -EXPORT void reim_fft_avx2_fma(const REIM_FFT_PRECOMP* tables, double* data) { UNDEFINED(); } -EXPORT void reim_ifft_avx2_fma(const REIM_IFFT_PRECOMP* tables, double* data) { UNDEFINED(); } - -// EXPORT void reim_fft(const REIM_FFT_PRECOMP* tables, double* data) { tables->function(tables, data); } -// EXPORT void reim_ifft(const REIM_IFFT_PRECOMP* tables, double* data) { tables->function(tables, data); }
@@ -1,234 +0,0 @@
-#ifndef SPQLIOS_REIM_FFT_H -#define SPQLIOS_REIM_FFT_H - -#include "../commons.h" - -typedef struct reim_fft_precomp REIM_FFT_PRECOMP; -typedef struct reim_ifft_precomp REIM_IFFT_PRECOMP; -typedef struct reim_add_precomp REIM_FFTVEC_ADD_PRECOMP; -typedef struct reim_sub_precomp REIM_FFTVEC_SUB_PRECOMP; -typedef struct reim_mul_precomp REIM_FFTVEC_MUL_PRECOMP; -typedef struct reim_addmul_precomp REIM_FFTVEC_ADDMUL_PRECOMP; -typedef struct reim_fftvec_automorphism_precomp REIM_FFTVEC_AUTOMORPHISM_PRECOMP; -typedef struct reim_from_znx32_precomp REIM_FROM_ZNX32_PRECOMP; -typedef struct reim_from_znx64_precomp REIM_FROM_ZNX64_PRECOMP; -typedef struct reim_from_tnx32_precomp REIM_FROM_TNX32_PRECOMP; -typedef struct reim_to_tnx32_precomp REIM_TO_TNX32_PRECOMP; -typedef struct reim_to_tnx_precomp REIM_TO_TNX_PRECOMP; -typedef struct reim_to_znx64_precomp REIM_TO_ZNX64_PRECOMP; - -/** - * @brief precomputes fft tables. - * The FFT tables contains a constant section that is required for efficient FFT operations in dimension nn. - * The resulting pointer is to be passed as "tables" argument to any call to the fft function. - * The user can optionnally allocate zero or more computation buffers, which are scratch spaces that are contiguous to - * the constant tables in memory, and allow for more efficient operations. It is the user's responsibility to ensure - * that each of those buffers are never used simultaneously by two ffts on different threads at the same time. The fft - * table must be deleted by delete_fft_precomp after its last usage. - */ -EXPORT REIM_FFT_PRECOMP* new_reim_fft_precomp(uint32_t m, uint32_t num_buffers); - -/** - * @brief gets the address of a fft buffer allocated during new_fft_precomp. - * This buffer can be used as data pointer in subsequent calls to fft, - * and does not need to be released afterwards. - */ -EXPORT double* reim_fft_precomp_get_buffer(const REIM_FFT_PRECOMP* tables, uint32_t buffer_index); - -/** - * @brief allocates a new fft buffer. - * This buffer can be used as data pointer in subsequent calls to fft, - * and must be deleted afterwards by calling delete_fft_buffer. - */ -EXPORT double* new_reim_fft_buffer(uint32_t m); - -/** - * @brief allocates a new fft buffer. - * This buffer can be used as data pointer in subsequent calls to fft, - * and must be deleted afterwards by calling delete_fft_buffer. - */ -EXPORT void delete_reim_fft_buffer(double* buffer); - -/** - * @brief deallocates a fft table and all its built-in buffers. - */ -#define delete_reim_fft_precomp free - -/** - * @brief computes a direct fft in-place over data. - */ -EXPORT void reim_fft(const REIM_FFT_PRECOMP* tables, double* data); - -EXPORT REIM_IFFT_PRECOMP* new_reim_ifft_precomp(uint32_t m, uint32_t num_buffers); -EXPORT double* reim_ifft_precomp_get_buffer(const REIM_IFFT_PRECOMP* tables, uint32_t buffer_index); -EXPORT void reim_ifft(const REIM_IFFT_PRECOMP* tables, double* data); -#define delete_reim_ifft_precomp free - -EXPORT REIM_FFTVEC_ADD_PRECOMP* new_reim_fftvec_add_precomp(uint32_t m); -EXPORT void reim_fftvec_add(const REIM_FFTVEC_ADD_PRECOMP* tables, double* r, const double* a, const double* b); -#define delete_reim_fftvec_add_precomp free - -EXPORT REIM_FFTVEC_SUB_PRECOMP* new_reim_fftvec_sub_precomp(uint32_t m); -EXPORT void reim_fftvec_sub(const REIM_FFTVEC_SUB_PRECOMP* tables, double* r, const double* a, const double* b); -#define delete_reim_fftvec_sub_precomp free - -EXPORT REIM_FFTVEC_MUL_PRECOMP* new_reim_fftvec_mul_precomp(uint32_t m); -EXPORT void reim_fftvec_mul(const REIM_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b); -#define delete_reim_fftvec_mul_precomp free - -EXPORT REIM_FFTVEC_ADDMUL_PRECOMP* new_reim_fftvec_addmul_precomp(uint32_t m); -EXPORT void reim_fftvec_addmul(const REIM_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, const double* b); -#define delete_reim_fftvec_addmul_precomp free - -EXPORT REIM_FFTVEC_AUTOMORPHISM_PRECOMP* new_reim_fftvec_automorphism_precomp(uint32_t m); -EXPORT void reim_fftvec_automorphism(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* tables, int64_t p, double* r, - const double* a, uint64_t a_size); - -EXPORT void reim_fftvec_automorphism_inplace(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* tables, int64_t p, double* a, - uint64_t a_size, uint8_t* tmp_bytes); - -EXPORT uint64_t reim_fftvec_automorphism_inplace_tmp_bytes(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* tables); - -#define delete_reim_fftvec_automorphism_precomp free - -/** - * @brief prepares a conversion from ZnX to the cplx layout. - * All the coefficients must be strictly lower than 2^log2bound in absolute value. Any attempt to use - * this function on a larger coefficient is undefined behaviour. The resulting precomputed data must - * be freed with `new_reim_from_znx32_precomp` - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the inputs have n=2m - * int32 coefficients in natural order modulo X^n+1 - * @param log2bound bound on the input coefficients. Must be between 0 and 32 - */ -EXPORT REIM_FROM_ZNX32_PRECOMP* new_reim_from_znx32_precomp(uint32_t m, uint32_t log2bound); - -/** - * @brief converts from ZnX to the cplx layout. - * @param tables precomputed data obtained by new_reim_from_znx32_precomp. - * @param r resulting array of m complexes coefficients mod X^m-i - * @param x input array of n bounded integer coefficients mod X^n+1 - */ -EXPORT void reim_from_znx32(const REIM_FROM_ZNX32_PRECOMP* tables, void* r, const int32_t* a); -/** @brief frees a precomputed conversion data initialized with new_reim_from_znx32_precomp. */ -#define delete_reim_from_znx32_precomp free - -/** - * @brief converts from ZnX to the cplx layout. - * @param tables precomputed data obtained by new_reim_from_znx64_precomp. - * @param r resulting array of m complexes coefficients mod X^m-i - * @param x input array of n bounded integer coefficients mod X^n+1 - */ -EXPORT void reim_from_znx64(const REIM_FROM_ZNX64_PRECOMP* tables, void* r, const int64_t* a); -/** @brief frees a precomputed conversion data initialized with new_reim_from_znx32_precomp. */ -EXPORT REIM_FROM_ZNX64_PRECOMP* new_reim_from_znx64_precomp(uint32_t m, uint32_t maxbnd); -#define delete_reim_from_znx64_precomp free -EXPORT void reim_from_znx64_simple(uint32_t m, uint32_t log2bound, void* r, const int64_t* a); - -/** - * @brief prepares a conversion from TnX to the cplx layout. - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the inputs have n=2m - * torus32 coefficients. The resulting precomputed data must - * be freed with `delete_reim_from_tnx32_precomp` - */ - -EXPORT REIM_FROM_TNX32_PRECOMP* new_reim_from_tnx32_precomp(uint32_t m); -/** - * @brief converts from TnX to the cplx layout. - * @param tables precomputed data obtained by new_reim_from_tnx32_precomp. - * @param r resulting array of m complexes coefficients mod X^m-i - * @param x input array of n torus32 coefficients mod X^n+1 - */ -EXPORT void reim_from_tnx32(const REIM_FROM_TNX32_PRECOMP* tables, void* r, const int32_t* a); -/** @brief frees a precomputed conversion data initialized with new_reim_from_tnx32_precomp. */ -#define delete_reim_from_tnx32_precomp free - -/** - * @brief prepares a rescale and conversion from the cplx layout to TnX. - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the outputs have n=2m - * torus32 coefficients. - * @param divisor must be a power of two. The inputs are rescaled by divisor before being reduced modulo 1. - * Remember that the output of an iFFT must be divided by m. - * @param log2overhead all inputs absolute values must be within divisor.2^log2overhead. - * For any inputs outside of these bounds, the conversion is undefined behaviour. - * The maximum supported log2overhead is 52, and the algorithm is faster for log2overhead=18. - */ -EXPORT REIM_TO_TNX32_PRECOMP* new_reim_to_tnx32_precomp(uint32_t m, double divisor, uint32_t log2overhead); - -/** - * @brief rescale, converts and reduce mod 1 from cplx layout to torus32. - * @param tables precomputed data obtained by new_reim_from_tnx32_precomp. - * @param r resulting array of n torus32 coefficients mod X^n+1 - * @param x input array of m cplx coefficients mod X^m-i - */ -EXPORT void reim_to_tnx32(const REIM_TO_TNX32_PRECOMP* tables, int32_t* r, const void* a); -#define delete_reim_to_tnx32_precomp free - -/** - * @brief prepares a rescale and conversion from the cplx layout to TnX (doubles). - * @param m the target complex dimension m from C[X] mod X^m-i. Note that the outputs have n=2m - * torus32 coefficients. - * @param divisor must be a power of two. The inputs are rescaled by divisor before being reduced modulo 1. - * Remember that the output of an iFFT must be divided by m. - * @param log2overhead all inputs absolute values must be within divisor.2^log2overhead. - * For any inputs outside of these bounds, the conversion is undefined behaviour. - * The maximum supported log2overhead is 52, and the algorithm is faster for log2overhead=18. - */ -EXPORT REIM_TO_TNX_PRECOMP* new_reim_to_tnx_precomp(uint32_t m, double divisor, uint32_t log2overhead); -/** - * @brief rescale, converts and reduce mod 1 from cplx layout to torus32. - * @param tables precomputed data obtained by new_reim_from_tnx32_precomp. - * @param r resulting array of n torus32 coefficients mod X^n+1 - * @param x input array of m cplx coefficients mod X^m-i - */ -EXPORT void reim_to_tnx(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* a); -#define delete_reim_to_tnx_precomp free -EXPORT void reim_to_tnx_simple(uint32_t m, double divisor, uint32_t log2overhead, double* r, const double* a); - -EXPORT REIM_TO_ZNX64_PRECOMP* new_reim_to_znx64_precomp(uint32_t m, double divisor, uint32_t log2bound); -#define delete_reim_to_znx64_precomp free -EXPORT void reim_to_znx64(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* a); -EXPORT void reim_to_znx64_simple(uint32_t m, double divisor, uint32_t log2bound, int64_t* r, const void* a); - -/** - * @brief Simpler API for the fft function. - * For each dimension, the precomputed tables for this dimension are generated automatically. - * It is advised to do one dry-run per desired dimension before using in a multithread environment */ -EXPORT void reim_fft_simple(uint32_t m, void* data); -/** - * @brief Simpler API for the ifft function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension in the main thread before using in a multithread - * environment */ -EXPORT void reim_ifft_simple(uint32_t m, void* data); -/** - * @brief Simpler API for the fftvec addition function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim_fftvec_add_simple(uint32_t m, void* r, const void* a, const void* b); -/** - * @brief Simpler API for the fftvec multiplication function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim_fftvec_mul_simple(uint32_t m, void* r, const void* a, const void* b); -/** - * @brief Simpler API for the fftvec addmul function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim_fftvec_addmul_simple(uint32_t m, void* r, const void* a, const void* b); -/** - * @brief Simpler API for the znx32 to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim_from_znx32_simple(uint32_t m, uint32_t log2bound, void* r, const int32_t* x); -/** - * @brief Simpler API for the tnx32 to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim_from_tnx32_simple(uint32_t m, void* r, const int32_t* x); -/** - * @brief Simpler API for the cplx to tnx32 conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim_to_tnx32_simple(uint32_t m, double divisor, uint32_t log2overhead, int32_t* r, const void* x); - -#endif // SPQLIOS_REIM_FFT_H
@@ -1,167 +0,0 @@
-#rdi datare ptr -#rsi dataim ptr -#rdx om ptr -.globl reim_fft16_avx_fma -reim_fft16_avx_fma: -vmovupd (%rdi),%ymm0 # ra0 -vmovupd 0x20(%rdi),%ymm1 # ra4 -vmovupd 0x40(%rdi),%ymm2 # ra8 -vmovupd 0x60(%rdi),%ymm3 # ra12 -vmovupd (%rsi),%ymm4 # ia0 -vmovupd 0x20(%rsi),%ymm5 # ia4 -vmovupd 0x40(%rsi),%ymm6 # ia8 -vmovupd 0x60(%rsi),%ymm7 # ia12 - -vmovupd (%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar -vmulpd %ymm6,%ymm13,%ymm8 # ia0.omai -vmulpd %ymm7,%ymm13,%ymm9 # ia4.omai -vmulpd %ymm2,%ymm13,%ymm10 # ra0.omai -vmulpd %ymm3,%ymm13,%ymm11 # ra4.omai -vfmsub231pd %ymm2,%ymm12,%ymm8 # rprod0 -vfmsub231pd %ymm3,%ymm12,%ymm9 # rprod4 -vfmadd231pd %ymm6,%ymm12,%ymm10 # iprod0 -vfmadd231pd %ymm7,%ymm12,%ymm11 # iprod4 -vsubpd %ymm8,%ymm0,%ymm2 -vsubpd %ymm9,%ymm1,%ymm3 -vsubpd %ymm10,%ymm4,%ymm6 -vsubpd %ymm11,%ymm5,%ymm7 -vaddpd %ymm8,%ymm0,%ymm0 -vaddpd %ymm9,%ymm1,%ymm1 -vaddpd %ymm10,%ymm4,%ymm4 -vaddpd %ymm11,%ymm5,%ymm5 - -1: -vmovupd 16(%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar -vmulpd %ymm5,%ymm13,%ymm8 # ia0.omai (tw) -vmulpd %ymm7,%ymm12,%ymm9 # ia4.omar (itw) -vmulpd %ymm1,%ymm13,%ymm10 # ra0.omai (tw) -vmulpd %ymm3,%ymm12,%ymm11 # ra4.omar (itw) -vfmsub231pd %ymm1,%ymm12,%ymm8 # rprod0 (tw) -vfmadd231pd %ymm3,%ymm13,%ymm9 # rprod4 (itw) -vfmadd231pd %ymm5,%ymm12,%ymm10 # iprod0 (tw) -vfmsub231pd %ymm7,%ymm13,%ymm11 # iprod4 (itw) -vsubpd %ymm8,%ymm0,%ymm1 -vaddpd %ymm9,%ymm2,%ymm3 -vsubpd %ymm10,%ymm4,%ymm5 -vaddpd %ymm11,%ymm6,%ymm7 -vaddpd %ymm8,%ymm0,%ymm0 -vsubpd %ymm9,%ymm2,%ymm2 -vaddpd %ymm10,%ymm4,%ymm4 -vsubpd %ymm11,%ymm6,%ymm6 - -2: -vmovupd 0x20(%rdx),%ymm12 -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omaiii'i' -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omarrr'r' - -vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) - -vmulpd %ymm10,%ymm13,%ymm4 # ia0.omai (tw) -vmulpd %ymm11,%ymm12,%ymm5 # ia4.omar (itw) -vmulpd %ymm8,%ymm13,%ymm6 # ra0.omai (tw) -vmulpd %ymm9,%ymm12,%ymm7 # ra4.omar (itw) -vfmsub231pd %ymm8,%ymm12,%ymm4 # rprod0 (tw) -vfmadd231pd %ymm9,%ymm13,%ymm5 # rprod4 (itw) -vfmadd231pd %ymm10,%ymm12,%ymm6 # iprod0 (tw) -vfmsub231pd %ymm11,%ymm13,%ymm7 # iprod4 (itw) -vsubpd %ymm4,%ymm0,%ymm8 -vaddpd %ymm5,%ymm1,%ymm9 -vsubpd %ymm6,%ymm2,%ymm10 -vaddpd %ymm7,%ymm3,%ymm11 -vaddpd %ymm4,%ymm0,%ymm0 -vsubpd %ymm5,%ymm1,%ymm1 -vaddpd %ymm6,%ymm2,%ymm2 -vsubpd %ymm7,%ymm3,%ymm3 - -#vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -#vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -#vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -#vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -#vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -#vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -#vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -#vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 - -3: -vmovupd 0x40(%rdx),%ymm12 -vmovupd 0x60(%rdx),%ymm13 - -#vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -#vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -#vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -#vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -#vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -#vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -#vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -#vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) - -vunpckhpd %ymm1,%ymm0,%ymm4 # (0,1) -> (0,4) -vunpckhpd %ymm3,%ymm2,%ymm6 # (2,3) -> (2,6) -vunpckhpd %ymm9,%ymm8,%ymm5 # (8,9) -> (1,5) -vunpckhpd %ymm11,%ymm10,%ymm7 # (10,11) -> (3,7) -vunpcklpd %ymm1,%ymm0,%ymm0 -vunpcklpd %ymm3,%ymm2,%ymm2 -vunpcklpd %ymm9,%ymm8,%ymm1 -vunpcklpd %ymm11,%ymm10,%ymm3 - -vmulpd %ymm6,%ymm13,%ymm8 # ia0.omai (tw) -vmulpd %ymm7,%ymm12,%ymm9 # ia4.omar (itw) -vmulpd %ymm4,%ymm13,%ymm10 # ra0.omai (tw) -vmulpd %ymm5,%ymm12,%ymm11 # ra4.omar (itw) -vfmsub231pd %ymm4,%ymm12,%ymm8 # rprod0 (tw) -vfmadd231pd %ymm5,%ymm13,%ymm9 # rprod4 (itw) -vfmadd231pd %ymm6,%ymm12,%ymm10 # iprod0 (tw) -vfmsub231pd %ymm7,%ymm13,%ymm11 # iprod4 (itw) -vsubpd %ymm8,%ymm0,%ymm4 -vaddpd %ymm9,%ymm1,%ymm5 -vsubpd %ymm10,%ymm2,%ymm6 -vaddpd %ymm11,%ymm3,%ymm7 -vaddpd %ymm8,%ymm0,%ymm0 -vsubpd %ymm9,%ymm1,%ymm1 -vaddpd %ymm10,%ymm2,%ymm2 -vsubpd %ymm11,%ymm3,%ymm3 - -vunpckhpd %ymm7,%ymm3,%ymm11 # (0,4) -> (0,1) -vunpckhpd %ymm5,%ymm1,%ymm9 # (2,6) -> (2,3) -vunpcklpd %ymm7,%ymm3,%ymm10 -vunpcklpd %ymm5,%ymm1,%ymm8 -vunpckhpd %ymm6,%ymm2,%ymm3 # (1,5) -> (8,9) -vunpckhpd %ymm4,%ymm0,%ymm1 # (3,7) -> (10,11) -vunpcklpd %ymm6,%ymm2,%ymm2 -vunpcklpd %ymm4,%ymm0,%ymm0 - -vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 - -4: -vmovupd %ymm0,(%rdi) # ra0 -vmovupd %ymm1,0x20(%rdi) # ra4 -vmovupd %ymm2,0x40(%rdi) # ra8 -vmovupd %ymm3,0x60(%rdi) # ra12 -vmovupd %ymm4,(%rsi) # ia0 -vmovupd %ymm5,0x20(%rsi) # ia4 -vmovupd %ymm6,0x40(%rsi) # ia8 -vmovupd %ymm7,0x60(%rsi) # ia12 -vzeroupper -ret -.size reim_fft16_avx_fma, .-reim_fft16_avx_fma -.section .note.GNU-stack,"",@progbits
@@ -1,203 +0,0 @@
- .text - .p2align 4 - .globl reim_fft16_avx_fma - .def reim_fft16_avx_fma; .scl 2; .type 32; .endef -reim_fft16_avx_fma: - - pushq %rdi - pushq %rsi - movq %rcx,%rdi - movq %rdx,%rsi - movq %r8,%rdx - subq $0x100,%rsp - movdqu %xmm6,(%rsp) - movdqu %xmm7,0x10(%rsp) - movdqu %xmm8,0x20(%rsp) - movdqu %xmm9,0x30(%rsp) - movdqu %xmm10,0x40(%rsp) - movdqu %xmm11,0x50(%rsp) - movdqu %xmm12,0x60(%rsp) - movdqu %xmm13,0x70(%rsp) - movdqu %xmm14,0x80(%rsp) - movdqu %xmm15,0x90(%rsp) - callq reim_fft16_avx_fma_amd64 - movdqu (%rsp),%xmm6 - movdqu 0x10(%rsp),%xmm7 - movdqu 0x20(%rsp),%xmm8 - movdqu 0x30(%rsp),%xmm9 - movdqu 0x40(%rsp),%xmm10 - movdqu 0x50(%rsp),%xmm11 - movdqu 0x60(%rsp),%xmm12 - movdqu 0x70(%rsp),%xmm13 - movdqu 0x80(%rsp),%xmm14 - movdqu 0x90(%rsp),%xmm15 - addq $0x100,%rsp - popq %rsi - popq %rdi - retq - -#rdi datare ptr -#rsi dataim ptr -#rdx om ptr -#.globl reim_fft16_avx_fma_amd64 -reim_fft16_avx_fma_amd64: -vmovupd (%rdi),%ymm0 # ra0 -vmovupd 0x20(%rdi),%ymm1 # ra4 -vmovupd 0x40(%rdi),%ymm2 # ra8 -vmovupd 0x60(%rdi),%ymm3 # ra12 -vmovupd (%rsi),%ymm4 # ia0 -vmovupd 0x20(%rsi),%ymm5 # ia4 -vmovupd 0x40(%rsi),%ymm6 # ia8 -vmovupd 0x60(%rsi),%ymm7 # ia12 - -vmovupd (%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar -vmulpd %ymm6,%ymm13,%ymm8 # ia0.omai -vmulpd %ymm7,%ymm13,%ymm9 # ia4.omai -vmulpd %ymm2,%ymm13,%ymm10 # ra0.omai -vmulpd %ymm3,%ymm13,%ymm11 # ra4.omai -vfmsub231pd %ymm2,%ymm12,%ymm8 # rprod0 -vfmsub231pd %ymm3,%ymm12,%ymm9 # rprod4 -vfmadd231pd %ymm6,%ymm12,%ymm10 # iprod0 -vfmadd231pd %ymm7,%ymm12,%ymm11 # iprod4 -vsubpd %ymm8,%ymm0,%ymm2 -vsubpd %ymm9,%ymm1,%ymm3 -vsubpd %ymm10,%ymm4,%ymm6 -vsubpd %ymm11,%ymm5,%ymm7 -vaddpd %ymm8,%ymm0,%ymm0 -vaddpd %ymm9,%ymm1,%ymm1 -vaddpd %ymm10,%ymm4,%ymm4 -vaddpd %ymm11,%ymm5,%ymm5 - -1: -vmovupd 16(%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar -vmulpd %ymm5,%ymm13,%ymm8 # ia0.omai (tw) -vmulpd %ymm7,%ymm12,%ymm9 # ia4.omar (itw) -vmulpd %ymm1,%ymm13,%ymm10 # ra0.omai (tw) -vmulpd %ymm3,%ymm12,%ymm11 # ra4.omar (itw) -vfmsub231pd %ymm1,%ymm12,%ymm8 # rprod0 (tw) -vfmadd231pd %ymm3,%ymm13,%ymm9 # rprod4 (itw) -vfmadd231pd %ymm5,%ymm12,%ymm10 # iprod0 (tw) -vfmsub231pd %ymm7,%ymm13,%ymm11 # iprod4 (itw) -vsubpd %ymm8,%ymm0,%ymm1 -vaddpd %ymm9,%ymm2,%ymm3 -vsubpd %ymm10,%ymm4,%ymm5 -vaddpd %ymm11,%ymm6,%ymm7 -vaddpd %ymm8,%ymm0,%ymm0 -vsubpd %ymm9,%ymm2,%ymm2 -vaddpd %ymm10,%ymm4,%ymm4 -vsubpd %ymm11,%ymm6,%ymm6 - -2: -vmovupd 0x20(%rdx),%ymm12 -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omaiii'i' -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omarrr'r' - -vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) - -vmulpd %ymm10,%ymm13,%ymm4 # ia0.omai (tw) -vmulpd %ymm11,%ymm12,%ymm5 # ia4.omar (itw) -vmulpd %ymm8,%ymm13,%ymm6 # ra0.omai (tw) -vmulpd %ymm9,%ymm12,%ymm7 # ra4.omar (itw) -vfmsub231pd %ymm8,%ymm12,%ymm4 # rprod0 (tw) -vfmadd231pd %ymm9,%ymm13,%ymm5 # rprod4 (itw) -vfmadd231pd %ymm10,%ymm12,%ymm6 # iprod0 (tw) -vfmsub231pd %ymm11,%ymm13,%ymm7 # iprod4 (itw) -vsubpd %ymm4,%ymm0,%ymm8 -vaddpd %ymm5,%ymm1,%ymm9 -vsubpd %ymm6,%ymm2,%ymm10 -vaddpd %ymm7,%ymm3,%ymm11 -vaddpd %ymm4,%ymm0,%ymm0 -vsubpd %ymm5,%ymm1,%ymm1 -vaddpd %ymm6,%ymm2,%ymm2 -vsubpd %ymm7,%ymm3,%ymm3 - -#vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -#vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -#vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -#vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -#vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -#vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -#vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -#vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 - -3: -vmovupd 0x40(%rdx),%ymm12 -vmovupd 0x60(%rdx),%ymm13 - -#vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -#vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -#vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -#vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -#vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -#vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -#vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -#vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) - -vunpckhpd %ymm1,%ymm0,%ymm4 # (0,1) -> (0,4) -vunpckhpd %ymm3,%ymm2,%ymm6 # (2,3) -> (2,6) -vunpckhpd %ymm9,%ymm8,%ymm5 # (8,9) -> (1,5) -vunpckhpd %ymm11,%ymm10,%ymm7 # (10,11) -> (3,7) -vunpcklpd %ymm1,%ymm0,%ymm0 -vunpcklpd %ymm3,%ymm2,%ymm2 -vunpcklpd %ymm9,%ymm8,%ymm1 -vunpcklpd %ymm11,%ymm10,%ymm3 - -vmulpd %ymm6,%ymm13,%ymm8 # ia0.omai (tw) -vmulpd %ymm7,%ymm12,%ymm9 # ia4.omar (itw) -vmulpd %ymm4,%ymm13,%ymm10 # ra0.omai (tw) -vmulpd %ymm5,%ymm12,%ymm11 # ra4.omar (itw) -vfmsub231pd %ymm4,%ymm12,%ymm8 # rprod0 (tw) -vfmadd231pd %ymm5,%ymm13,%ymm9 # rprod4 (itw) -vfmadd231pd %ymm6,%ymm12,%ymm10 # iprod0 (tw) -vfmsub231pd %ymm7,%ymm13,%ymm11 # iprod4 (itw) -vsubpd %ymm8,%ymm0,%ymm4 -vaddpd %ymm9,%ymm1,%ymm5 -vsubpd %ymm10,%ymm2,%ymm6 -vaddpd %ymm11,%ymm3,%ymm7 -vaddpd %ymm8,%ymm0,%ymm0 -vsubpd %ymm9,%ymm1,%ymm1 -vaddpd %ymm10,%ymm2,%ymm2 -vsubpd %ymm11,%ymm3,%ymm3 - -vunpckhpd %ymm7,%ymm3,%ymm11 # (0,4) -> (0,1) -vunpckhpd %ymm5,%ymm1,%ymm9 # (2,6) -> (2,3) -vunpcklpd %ymm7,%ymm3,%ymm10 -vunpcklpd %ymm5,%ymm1,%ymm8 -vunpckhpd %ymm6,%ymm2,%ymm3 # (1,5) -> (8,9) -vunpckhpd %ymm4,%ymm0,%ymm1 # (3,7) -> (10,11) -vunpcklpd %ymm6,%ymm2,%ymm2 -vunpcklpd %ymm4,%ymm0,%ymm0 - -vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 - -4: -vmovupd %ymm0,(%rdi) # ra0 -vmovupd %ymm1,0x20(%rdi) # ra4 -vmovupd %ymm2,0x40(%rdi) # ra8 -vmovupd %ymm3,0x60(%rdi) # ra12 -vmovupd %ymm4,(%rsi) # ia0 -vmovupd %ymm5,0x20(%rsi) # ia4 -vmovupd %ymm6,0x40(%rsi) # ia8 -vmovupd %ymm7,0x60(%rsi) # ia12 -vzeroupper -ret
@@ -1,66 +0,0 @@
-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim_fft_private.h" - -__always_inline void reim_ctwiddle_avx_fma(__m128d* ra, __m128d* rb, __m128d* ia, __m128d* ib, const __m128d omre, - const __m128d omim) { - // rb * omre - ib * omim; - __m128d rprod0 = _mm_mul_pd(*ib, omim); - rprod0 = _mm_fmsub_pd(*rb, omre, rprod0); - - // rb * omim + ib * omre; - __m128d iprod0 = _mm_mul_pd(*rb, omim); - iprod0 = _mm_fmadd_pd(*ib, omre, iprod0); - - *rb = _mm_sub_pd(*ra, rprod0); - *ib = _mm_sub_pd(*ia, iprod0); - *ra = _mm_add_pd(*ra, rprod0); - *ia = _mm_add_pd(*ia, iprod0); -} - -EXPORT void reim_fft4_avx_fma(double* dre, double* dim, const void* ompv) { - const double* omp = (const double*)ompv; - - __m128d ra0 = _mm_loadu_pd(dre); - __m128d ra2 = _mm_loadu_pd(dre + 2); - __m128d ia0 = _mm_loadu_pd(dim); - __m128d ia2 = _mm_loadu_pd(dim + 2); - - // 1 - { - // duplicate omegas in precomp? - __m128d om = _mm_loadu_pd(omp); - __m128d omre = _mm_permute_pd(om, 0); - __m128d omim = _mm_permute_pd(om, 3); - - reim_ctwiddle_avx_fma(&ra0, &ra2, &ia0, &ia2, omre, omim); - } - - // 2 - { - const __m128d fft4neg = _mm_castsi128_pd(_mm_set_epi64x(UINT64_C(1) << 63, 0)); - __m128d om = _mm_loadu_pd(omp + 2); // om: r,i - __m128d omim = _mm_permute_pd(om, 1); // omim: i,r - __m128d omre = _mm_xor_pd(om, fft4neg); // omre: r,-i - - __m128d rb = _mm_unpackhi_pd(ra0, ra2); // (r0, r1), (r2, r3) -> (r1, r3) - __m128d ib = _mm_unpackhi_pd(ia0, ia2); // (i0, i1), (i2, i3) -> (i1, i3) - __m128d ra = _mm_unpacklo_pd(ra0, ra2); // (r0, r1), (r2, r3) -> (r0, r2) - __m128d ia = _mm_unpacklo_pd(ia0, ia2); // (i0, i1), (i2, i3) -> (i0, i2) - - reim_ctwiddle_avx_fma(&ra, &rb, &ia, &ib, omre, omim); - - ra0 = _mm_unpacklo_pd(ra, rb); - ia0 = _mm_unpacklo_pd(ia, ib); - ra2 = _mm_unpackhi_pd(ra, rb); - ia2 = _mm_unpackhi_pd(ia, ib); - } - - // 4 - _mm_storeu_pd(dre, ra0); - _mm_storeu_pd(dre + 2, ra2); - _mm_storeu_pd(dim, ia0); - _mm_storeu_pd(dim + 2, ia2); -}
@@ -1,89 +0,0 @@
-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim_fft_private.h" - -__always_inline void reim_ctwiddle_avx_fma(__m256d* ra, __m256d* rb, __m256d* ia, __m256d* ib, const __m256d omre, - const __m256d omim) { - // rb * omre - ib * omim; - __m256d rprod0 = _mm256_mul_pd(*ib, omim); - rprod0 = _mm256_fmsub_pd(*rb, omre, rprod0); - - // rb * omim + ib * omre; - __m256d iprod0 = _mm256_mul_pd(*rb, omim); - iprod0 = _mm256_fmadd_pd(*ib, omre, iprod0); - - *rb = _mm256_sub_pd(*ra, rprod0); - *ib = _mm256_sub_pd(*ia, iprod0); - *ra = _mm256_add_pd(*ra, rprod0); - *ia = _mm256_add_pd(*ia, iprod0); -} - -EXPORT void reim_fft8_avx_fma(double* dre, double* dim, const void* ompv) { - const double* omp = (const double*)ompv; - - __m256d ra0 = _mm256_loadu_pd(dre); - __m256d ra4 = _mm256_loadu_pd(dre + 4); - __m256d ia0 = _mm256_loadu_pd(dim); - __m256d ia4 = _mm256_loadu_pd(dim + 4); - - // 1 - { - // duplicate omegas in precomp? - __m128d omri = _mm_loadu_pd(omp); - __m256d omriri = _mm256_set_m128d(omri, omri); - __m256d omi = _mm256_permute_pd(omriri, 15); - __m256d omr = _mm256_permute_pd(omriri, 0); - - reim_ctwiddle_avx_fma(&ra0, &ra4, &ia0, &ia4, omr, omi); - } - - // 2 - { - const __m128d fft8neg = _mm_castsi128_pd(_mm_set_epi64x(UINT64_C(1) << 63, 0)); - __m128d omri = _mm_loadu_pd(omp + 2); // r,i - __m128d omrmi = _mm_xor_pd(omri, fft8neg); // r,-i - __m256d omrirmi = _mm256_set_m128d(omrmi, omri); // r,i,r,-i - __m256d omi = _mm256_permute_pd(omrirmi, 3); // i,i,r,r - __m256d omr = _mm256_permute_pd(omrirmi, 12); // r,r,-i,-i - - __m256d rb = _mm256_permute2f128_pd(ra0, ra4, 0x31); - __m256d ib = _mm256_permute2f128_pd(ia0, ia4, 0x31); - __m256d ra = _mm256_permute2f128_pd(ra0, ra4, 0x20); - __m256d ia = _mm256_permute2f128_pd(ia0, ia4, 0x20); - - reim_ctwiddle_avx_fma(&ra, &rb, &ia, &ib, omr, omi); - - ra0 = _mm256_permute2f128_pd(ra, rb, 0x20); - ra4 = _mm256_permute2f128_pd(ra, rb, 0x31); - ia0 = _mm256_permute2f128_pd(ia, ib, 0x20); - ia4 = _mm256_permute2f128_pd(ia, ib, 0x31); - } - - // 3 - { - const __m256d fft8neg2 = _mm256_castsi256_pd(_mm256_set_epi64x(UINT64_C(1) << 63, UINT64_C(1) << 63, 0, 0)); - __m256d om = _mm256_loadu_pd(omp + 4); // r0,r1,i0,i1 - __m256d omi = _mm256_permute2f128_pd(om, om, 1); // i0,i1,r0,r1 - __m256d omr = _mm256_xor_pd(om, fft8neg2); // r0,r1,-i0,-i1 - - __m256d rb = _mm256_unpackhi_pd(ra0, ra4); - __m256d ib = _mm256_unpackhi_pd(ia0, ia4); - __m256d ra = _mm256_unpacklo_pd(ra0, ra4); - __m256d ia = _mm256_unpacklo_pd(ia0, ia4); - - reim_ctwiddle_avx_fma(&ra, &rb, &ia, &ib, omr, omi); - - ra4 = _mm256_unpackhi_pd(ra, rb); - ia4 = _mm256_unpackhi_pd(ia, ib); - ra0 = _mm256_unpacklo_pd(ra, rb); - ia0 = _mm256_unpacklo_pd(ia, ib); - } - - // 4 - _mm256_storeu_pd(dre, ra0); - _mm256_storeu_pd(dre + 4, ra4); - _mm256_storeu_pd(dim, ia0); - _mm256_storeu_pd(dim + 4, ia4); -}
@@ -1,162 +0,0 @@
-#include "immintrin.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -__always_inline void reim_twiddle_fft_avx2_fma(uint32_t h, double* re, double* im, double om[2]) { - const __m128d omx = _mm_load_pd(om); - const __m256d omra = _mm256_set_m128d(omx, omx); - const __m256d omi = _mm256_unpackhi_pd(omra, omra); - const __m256d omr = _mm256_unpacklo_pd(omra, omra); - double* r0 = re; - double* r1 = re + h; - double* i0 = im; - double* i1 = im + h; - for (uint32_t i = 0; i < h; i += 4) { - __m256d ur0 = _mm256_loadu_pd(r0 + i); - __m256d ur1 = _mm256_loadu_pd(r1 + i); - __m256d ui0 = _mm256_loadu_pd(i0 + i); - __m256d ui1 = _mm256_loadu_pd(i1 + i); - __m256d tra = _mm256_mul_pd(omi, ui1); - __m256d tia = _mm256_mul_pd(omi, ur1); - tra = _mm256_fmsub_pd(omr, ur1, tra); - tia = _mm256_fmadd_pd(omr, ui1, tia); - ur1 = _mm256_sub_pd(ur0, tra); - ui1 = _mm256_sub_pd(ui0, tia); - ur0 = _mm256_add_pd(ur0, tra); - ui0 = _mm256_add_pd(ui0, tia); - _mm256_storeu_pd(r0 + i, ur0); - _mm256_storeu_pd(r1 + i, ur1); - _mm256_storeu_pd(i0 + i, ui0); - _mm256_storeu_pd(i1 + i, ui1); - } -} - -__always_inline void reim_bitwiddle_fft_avx2_fma(uint32_t h, double* re, double* im, double om[4]) { - double* const r0 = re; - double* const r1 = re + h; - double* const r2 = re + 2 * h; - double* const r3 = re + 3 * h; - double* const i0 = im; - double* const i1 = im + h; - double* const i2 = im + 2 * h; - double* const i3 = im + 3 * h; - const __m256d om0 = _mm256_loadu_pd(om); - const __m256d omb = _mm256_permute2f128_pd(om0, om0, 0x11); - const __m256d oma = _mm256_permute2f128_pd(om0, om0, 0x00); - const __m256d omai = _mm256_unpackhi_pd(oma, oma); - const __m256d omar = _mm256_unpacklo_pd(oma, oma); - const __m256d ombi = _mm256_unpackhi_pd(omb, omb); - const __m256d ombr = _mm256_unpacklo_pd(omb, omb); - for (uint32_t i = 0; i < h; i += 4) { - __m256d ur0 = _mm256_loadu_pd(r0 + i); - __m256d ur1 = _mm256_loadu_pd(r1 + i); - __m256d ur2 = _mm256_loadu_pd(r2 + i); - __m256d ur3 = _mm256_loadu_pd(r3 + i); - __m256d ui0 = _mm256_loadu_pd(i0 + i); - __m256d ui1 = _mm256_loadu_pd(i1 + i); - __m256d ui2 = _mm256_loadu_pd(i2 + i); - __m256d ui3 = _mm256_loadu_pd(i3 + i); - //------ twiddles 1 - __m256d tra = _mm256_mul_pd(omai, ui2); - __m256d trb = _mm256_mul_pd(omai, ui3); - __m256d tia = _mm256_mul_pd(omai, ur2); - __m256d tib = _mm256_mul_pd(omai, ur3); - tra = _mm256_fmsub_pd(omar, ur2, tra); - trb = _mm256_fmsub_pd(omar, ur3, trb); - tia = _mm256_fmadd_pd(omar, ui2, tia); - tib = _mm256_fmadd_pd(omar, ui3, tib); - ur2 = _mm256_sub_pd(ur0, tra); - ur3 = _mm256_sub_pd(ur1, trb); - ui2 = _mm256_sub_pd(ui0, tia); - ui3 = _mm256_sub_pd(ui1, tib); - ur0 = _mm256_add_pd(ur0, tra); - ur1 = _mm256_add_pd(ur1, trb); - ui0 = _mm256_add_pd(ui0, tia); - ui1 = _mm256_add_pd(ui1, tib); - //------ twiddles 1 - tra = _mm256_mul_pd(ombi, ui1); - trb = _mm256_mul_pd(ombr, ui3); // ii - tia = _mm256_mul_pd(ombi, ur1); - tib = _mm256_mul_pd(ombr, ur3); // ri - tra = _mm256_fmsub_pd(ombr, ur1, tra); - trb = _mm256_fmadd_pd(ombi, ur3, trb); //-rr+ii - tia = _mm256_fmadd_pd(ombr, ui1, tia); - tib = _mm256_fmsub_pd(ombi, ui3, tib); //-ir-ri - ur1 = _mm256_sub_pd(ur0, tra); - ur3 = _mm256_add_pd(ur2, trb); - ui1 = _mm256_sub_pd(ui0, tia); - ui3 = _mm256_add_pd(ui2, tib); - ur0 = _mm256_add_pd(ur0, tra); - ur2 = _mm256_sub_pd(ur2, trb); - ui0 = _mm256_add_pd(ui0, tia); - ui2 = _mm256_sub_pd(ui2, tib); - ///--- - _mm256_storeu_pd(r0 + i, ur0); - _mm256_storeu_pd(r1 + i, ur1); - _mm256_storeu_pd(r2 + i, ur2); - _mm256_storeu_pd(r3 + i, ur3); - _mm256_storeu_pd(i0 + i, ui0); - _mm256_storeu_pd(i1 + i, ui1); - _mm256_storeu_pd(i2 + i, ui2); - _mm256_storeu_pd(i3 + i, ui3); - } -} - -void reim_fft_bfs_16_avx2_fma(uint32_t m, double* re, double* im, double** omg) { - uint32_t log2m = _mm_popcnt_u32(m - 1); // log2(m); - uint32_t mm = m; - if ((log2m & 1) != 0) { - uint32_t h = mm >> 1; - // do the first twiddle iteration normally - reim_twiddle_fft_avx2_fma(h, re, im, *omg); - *omg += 2; - mm = h; - } - while (mm > 16) { - uint32_t h = mm >> 2; - for (uint32_t off = 0; off < m; off += mm) { - reim_bitwiddle_fft_avx2_fma(h, re + off, im + off, *omg); - *omg += 4; - } - mm = h; - } - if (mm != 16) abort(); // bug! - for (uint32_t off = 0; off < m; off += 16) { - reim_fft16_avx_fma(re + off, im + off, *omg); - *omg += 16; - } -} - -void reim_fft_rec_16_avx2_fma(uint32_t m, double* re, double* im, double** omg) { - if (m <= 2048) return reim_fft_bfs_16_avx2_fma(m, re, im, omg); - const uint32_t h = m / 2; - reim_twiddle_fft_avx2_fma(h, re, im, *omg); - *omg += 2; - reim_fft_rec_16_avx2_fma(h, re, im, omg); - reim_fft_rec_16_avx2_fma(h, re + h, im + h, omg); -} - -void reim_fft_avx2_fma(const REIM_FFT_PRECOMP* precomp, double* dat) { - const int32_t m = precomp->m; - double* omg = precomp->powomegas; - double* re = dat; - double* im = dat + m; - if (m <= 16) { - switch (m) { - case 1: - return; - case 2: - return reim_fft2_ref(re, im, omg); - case 4: - return reim_fft4_avx_fma(re, im, omg); - case 8: - return reim_fft8_avx_fma(re, im, omg); - case 16: - return reim_fft16_avx_fma(re, im, omg); - default: - abort(); // m is not a power of 2 - } - } - if (m <= 2048) return reim_fft_bfs_16_avx2_fma(m, re, im, &omg); - return reim_fft_rec_16_avx2_fma(m, re, im, &omg); -}
@@ -1,162 +0,0 @@
-#ifndef SPQLIOS_REIM_FFT_CORE_TEMPLATE_H -#define SPQLIOS_REIM_FFT_CORE_TEMPLATE_H - -// this file contains the main template for the fft strategy. -// it is meant to be included once for each specialization (ref, avx, neon) -// all the leaf functions it uses shall be defined in the following macros, -// before including this header. - -#if !defined(reim_fft16_f) || !defined(reim_fft16_pom_offset) || !defined(fill_reim_fft16_omegas_f) -#error "missing reim16 definitions" -#endif -#if !defined(reim_fft8_f) || !defined(reim_fft8_pom_offset) || !defined(fill_reim_fft8_omegas_f) -#error "missing reim8 definitions" -#endif -#if !defined(reim_fft4_f) || !defined(reim_fft4_pom_offset) || !defined(fill_reim_fft4_omegas_f) -#error "missing reim4 definitions" -#endif -#if !defined(reim_fft2_f) || !defined(reim_fft2_pom_offset) || !defined(fill_reim_fft2_omegas_f) -#error "missing reim2 definitions" -#endif -#if !defined(reim_twiddle_fft_f) || !defined(reim_twiddle_fft_pom_offset) || !defined(fill_reim_twiddle_fft_omegas_f) -#error "missing twiddle definitions" -#endif -#if !defined(reim_bitwiddle_fft_f) || !defined(reim_bitwiddle_fft_pom_offset) || \ - !defined(fill_reim_bitwiddle_fft_omegas_f) -#error "missing bitwiddle definitions" -#endif -#if !defined(reim_fft_bfs_16_f) || !defined(fill_reim_fft_bfs_16_omegas_f) -#error "missing bfs_16 definitions" -#endif -#if !defined(reim_fft_rec_16_f) || !defined(fill_reim_fft_rec_16_omegas_f) -#error "missing rec_16 definitions" -#endif -#if !defined(reim_fft_f) || !defined(fill_reim_fft_omegas_f) -#error "missing main definitions" -#endif - -void reim_fft_bfs_16_f(uint64_t m, double* re, double* im, double** omg) { - uint64_t log2m = log2(m); - uint64_t mm = m; - if (log2m & 1) { - uint64_t h = mm >> 1; - // do the first twiddle iteration normally - reim_twiddle_fft_f(h, re, im, *omg); - *omg += reim_twiddle_fft_pom_offset; - mm = h; - } - while (mm > 16) { - uint64_t h = mm >> 2; - for (uint64_t off = 0; off < m; off += mm) { - reim_bitwiddle_fft_f(h, re + off, im + off, *omg); - *omg += reim_bitwiddle_fft_pom_offset; - } - mm = h; - } - if (mm != 16) abort(); // bug! - for (uint64_t off = 0; off < m; off += 16) { - reim_fft16_f(re + off, im + off, *omg); - *omg += reim_fft16_pom_offset; - } -} - -void fill_reim_fft_bfs_16_omegas_f(uint64_t m, double entry_pwr, double** omg) { - uint64_t log2m = log2(m); - uint64_t mm = m; - double ss = entry_pwr; - if (log2m % 2 != 0) { - uint64_t h = mm >> 1; - double s = ss / 2.; - // do the first twiddle iteration normally - fill_reim_twiddle_fft_omegas_f(s, omg); - mm = h; - ss = s; - } - while (mm > 16) { - uint64_t h = mm >> 2; - double s = ss / 4.; - for (uint64_t off = 0; off < m; off += mm) { - double rs0 = s + fracrevbits(off / mm) / 4.; - fill_reim_bitwiddle_fft_omegas_f(rs0, omg); - } - mm = h; - ss = s; - } - if (mm != 16) abort(); // bug! - for (uint64_t off = 0; off < m; off += 16) { - double s = ss + fracrevbits(off / 16); - fill_reim_fft16_omegas_f(s, omg); - } -} - -void reim_fft_rec_16_f(uint64_t m, double* re, double* im, double** omg) { - if (m <= 2048) return reim_fft_bfs_16_f(m, re, im, omg); - const uint32_t h = m >> 1; - reim_twiddle_fft_f(h, re, im, *omg); - *omg += reim_twiddle_fft_pom_offset; - reim_fft_rec_16_f(h, re, im, omg); - reim_fft_rec_16_f(h, re + h, im + h, omg); -} - -void fill_reim_fft_rec_16_omegas_f(uint64_t m, double entry_pwr, double** omg) { - if (m <= 2048) return fill_reim_fft_bfs_16_omegas_f(m, entry_pwr, omg); - const uint64_t h = m / 2; - const double s = entry_pwr / 2; - fill_reim_twiddle_fft_omegas_f(s, omg); - fill_reim_fft_rec_16_omegas_f(h, s, omg); - fill_reim_fft_rec_16_omegas_f(h, s + 0.5, omg); -} - -void reim_fft_f(const REIM_FFT_PRECOMP* precomp, double* dat) { - const int32_t m = precomp->m; - double* omg = precomp->powomegas; - double* re = dat; - double* im = dat + m; - if (m <= 16) { - switch (m) { - case 1: - return; - case 2: - return reim_fft2_f(re, im, omg); - case 4: - return reim_fft4_f(re, im, omg); - case 8: - return reim_fft8_f(re, im, omg); - case 16: - return reim_fft16_f(re, im, omg); - default: - abort(); // m is not a power of 2 - } - } - if (m <= 2048) return reim_fft_bfs_16_f(m, re, im, &omg); - return reim_fft_rec_16_f(m, re, im, &omg); -} - -void fill_reim_fft_omegas_f(uint64_t m, double entry_pwr, double** omg) { - if (m <= 16) { - switch (m) { - case 1: - break; - case 2: - fill_reim_fft2_omegas_f(entry_pwr, omg); - break; - case 4: - fill_reim_fft4_omegas_f(entry_pwr, omg); - break; - case 8: - fill_reim_fft8_omegas_f(entry_pwr, omg); - break; - case 16: - fill_reim_fft16_omegas_f(entry_pwr, omg); - break; - default: - abort(); // m is not a power of 2 - } - } else if (m <= 2048) { - fill_reim_fft_bfs_16_omegas_f(m, entry_pwr, omg); - } else { - fill_reim_fft_rec_16_omegas_f(m, entry_pwr, omg); - } -} - -#endif // SPQLIOS_REIM_FFT_CORE_TEMPLATE_H
@@ -1,35 +0,0 @@
-#include <math.h> -#include <stdint.h> -#include <stdlib.h> -#include <string.h> - -#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -double accurate_cos(int32_t i, int32_t n) { // cos(2pi*i/n) - i = ((i % n) + n) % n; - if (i >= 3 * n / 4) return cos(2. * M_PI * (n - i) / (double)(n)); - if (i >= 2 * n / 4) return -cos(2. * M_PI * (i - n / 2) / (double)(n)); - if (i >= 1 * n / 4) return -cos(2. * M_PI * (n / 2 - i) / (double)(n)); - return cos(2. * M_PI * (i) / (double)(n)); -} - -double accurate_sin(int32_t i, int32_t n) { // sin(2pi*i/n) - i = ((i % n) + n) % n; - if (i >= 3 * n / 4) return -sin(2. * M_PI * (n - i) / (double)(n)); - if (i >= 2 * n / 4) return -sin(2. * M_PI * (i - n / 2) / (double)(n)); - if (i >= 1 * n / 4) return sin(2. * M_PI * (n / 2 - i) / (double)(n)); - return sin(2. * M_PI * (i) / (double)(n)); -} - -EXPORT double* reim_ifft_precomp_get_buffer(const REIM_IFFT_PRECOMP* tables, uint32_t buffer_index) { - return (double*)((uint8_t*)tables->aligned_buffers + buffer_index * tables->buf_size); -} - -EXPORT double* reim_fft_precomp_get_buffer(const REIM_FFT_PRECOMP* tables, uint32_t buffer_index) { - return (double*)((uint8_t*)tables->aligned_buffers + buffer_index * tables->buf_size); -} - -EXPORT void reim_fft(const REIM_FFT_PRECOMP* tables, double* data) { tables->function(tables, data); } -EXPORT void reim_ifft(const REIM_IFFT_PRECOMP* tables, double* data) { tables->function(tables, data); }
@@ -1,147 +0,0 @@
-#ifndef SPQLIOS_REIM_FFT_INTERNAL_H -#define SPQLIOS_REIM_FFT_INTERNAL_H - -#include <math.h> -#include <stdint.h> -#include <stdlib.h> - -#include "reim_fft.h" - -EXPORT void reim_fftvec_addmul_fma(const REIM_FFTVEC_ADDMUL_PRECOMP* precomp, double* r, const double* a, - const double* b); -EXPORT void reim_fftvec_addmul_ref(const REIM_FFTVEC_ADDMUL_PRECOMP* precomp, double* r, const double* a, - const double* b); - -EXPORT void reim_fftvec_mul_fma(const REIM_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b); -EXPORT void reim_fftvec_mul_ref(const REIM_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b); - -EXPORT void reim_fftvec_add_ref(const REIM_FFTVEC_ADD_PRECOMP* tables, double* r, const double* a, const double* b); -EXPORT void reim_fftvec_add_fma(const REIM_FFTVEC_ADD_PRECOMP* tables, double* r, const double* a, const double* b); - -EXPORT void reim_fftvec_sub_ref(const REIM_FFTVEC_SUB_PRECOMP* tables, double* r, const double* a, const double* b); -EXPORT void reim_fftvec_sub_fma(const REIM_FFTVEC_SUB_PRECOMP* tables, double* r, const double* a, const double* b); - -/** @brief r = x from ZnX (coeffs as signed int32_t's ) to double */ -EXPORT void reim_from_znx32_ref(const REIM_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x); -EXPORT void reim_from_znx32_avx2_fma(const REIM_FROM_ZNX32_PRECOMP* precomp, void* r, const int32_t* x); - -/** @brief r = x mod 1 (coeffs as double) */ -EXPORT void reim_to_tnx_ref(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* x); -EXPORT void reim_to_tnx_avx(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* x); - -EXPORT void reim_from_znx64_ref(const REIM_FROM_ZNX64_PRECOMP* precomp, void* r, const int64_t* x); -EXPORT void reim_from_znx64_bnd50_fma(const REIM_FROM_ZNX64_PRECOMP* precomp, void* r, const int64_t* x); - -EXPORT void reim_to_znx64_ref(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* x); -EXPORT void reim_to_znx64_avx2_bnd63_fma(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* x); -EXPORT void reim_to_znx64_avx2_bnd50_fma(const REIM_TO_ZNX64_PRECOMP* precomp, int64_t* r, const void* x); - -/** - * @brief compute the fft evaluations of P in place - * fft(data) = fft_rec(data, i); - * function fft_rec(data, omega) { - * if #data = 1: return data - * let s = sqrt(omega) w. re(s)>0 - * let (u,v) = merge_fft(data, s) - * return [fft_rec(u, s), fft_rec(v, -s)] - * } - * @param tables precomputed tables (contains all the powers of omega in the order they are used) - * @param data vector of m complexes (coeffs as input, evals as output) - */ -EXPORT void reim_fft_ref(const REIM_FFT_PRECOMP* tables, double* data); -EXPORT void reim_fft_avx2_fma(const REIM_FFT_PRECOMP* tables, double* data); - -/** - * @brief compute the ifft evaluations of P in place - * ifft(data) = ifft_rec(data, i); - * function ifft_rec(data, omega) { - * if #data = 1: return data - * let s = sqrt(omega) w. re(s)>0 - * let (u,v) = data - * return split_fft([ifft_rec(u, s), ifft_rec(v, -s)],s) - * } - * @param itables precomputed tables (contains all the powers of omega in the order they are used) - * @param data vector of m complexes (coeffs as input, evals as output) - */ -EXPORT void reim_ifft_ref(const REIM_IFFT_PRECOMP* itables, double* data); -EXPORT void reim_ifft_avx2_fma(const REIM_IFFT_PRECOMP* itables, double* data); - -/// new compressed implementation - -/** @brief naive FFT code mod X^m-exp(2i.pi.entry_pwr) */ -EXPORT void reim_naive_fft(uint64_t m, double entry_pwr, double* re, double* im); - -/** @brief 16-dimensional FFT with precomputed omegas */ -EXPORT void reim_fft16_neon(double* dre, double* dim, const void* omega); -EXPORT void reim_fft16_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_fft16_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft16 functions */ -EXPORT void fill_reim_fft16_omegas(const double entry_pwr, double** omg); -EXPORT void fill_reim_fft16_omegas_neon(const double entry_pwr, double** omg); - -/** @brief 8-dimensional FFT with precomputed omegas */ -EXPORT void reim_fft8_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_fft8_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft8 functions */ -EXPORT void fill_reim_fft8_omegas(const double entry_pwr, double** omg); - -/** @brief 4-dimensional FFT with precomputed omegas */ -EXPORT void reim_fft4_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_fft4_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft8 functions */ -EXPORT void fill_reim_fft4_omegas(const double entry_pwr, double** omg); - -/** @brief 2-dimensional FFT with precomputed omegas */ -// EXPORT void reim_fft4_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_fft2_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft8 functions */ -EXPORT void fill_reim_fft2_omegas(const double entry_pwr, double** omg); - -EXPORT void reim_fft_bfs_16_ref(uint64_t m, double* re, double* im, double** omg); -EXPORT void fill_reim_fft_bfs_16_omegas(uint64_t m, double entry_pwr, double** omg); - -EXPORT void reim_fft_rec_16_ref(uint64_t m, double* re, double* im, double** omg); -EXPORT void fill_reim_fft_rec_16_omegas(uint64_t m, double entry_pwr, double** omg); - -/** @brief naive FFT code mod X^m-exp(2i.pi.entry_pwr) */ -EXPORT void reim_naive_ifft(uint64_t m, double entry_pwr, double* re, double* im); - -/** @brief 16-dimensional FFT with precomputed omegas */ -EXPORT void reim_ifft16_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_ifft16_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft16 functions */ -EXPORT void fill_reim_ifft16_omegas(const double entry_pwr, double** omg); - -/** @brief 8-dimensional FFT with precomputed omegas */ -EXPORT void reim_ifft8_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_ifft8_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft8 functions */ -EXPORT void fill_reim_ifft8_omegas(const double entry_pwr, double** omg); - -/** @brief 4-dimensional FFT with precomputed omegas */ -EXPORT void reim_ifft4_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_ifft4_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft8 functions */ -EXPORT void fill_reim_ifft4_omegas(const double entry_pwr, double** omg); - -/** @brief 2-dimensional FFT with precomputed omegas */ -// EXPORT void reim_ifft2_avx_fma(double* dre, double* dim, const void* omega); -EXPORT void reim_ifft2_ref(double* dre, double* dim, const void* omega); - -/** @brief precompute omegas so that reim_fft8 functions */ -EXPORT void fill_reim_ifft2_omegas(const double entry_pwr, double** omg); - -EXPORT void reim_ifft_bfs_16_ref(uint64_t m, double* re, double* im, double** omg); -EXPORT void fill_reim_ifft_bfs_16_omegas(uint64_t m, double entry_pwr, double** omg); - -EXPORT void reim_ifft_rec_16_ref(uint64_t m, double* re, double* im, double** omg); -EXPORT void fill_reim_ifft_rec_16_omegas(uint64_t m, double entry_pwr, double** omg); - -#endif // SPQLIOS_REIM_FFT_INTERNAL_H
@@ -1,1624 +0,0 @@
-/* - * This file is adapted from the implementation of the FFT on Arm64/Neon - * available in https://github.com/cothan/Falcon-Arm (neon/fft.c). - * ============================================================================= - * Copyright (c) 2022 by Cryptographic Engineering Research Group (CERG) - * ECE Department, George Mason University - * Fairfax, VA, U.S.A. - * @author: Duc Tri Nguyen dnguye69@gmu.edu, cothannguyen@gmail.com - * Licensed under the Apache License, Version 2.0 (the "License"); - * ============================================================================= - * - * The original source file has been modified by the authors of spqlios-arithmetic - * to be interfaced with the dynamic twiddle factors generator of the spqlios-arithmetic - * library, as well as the recursive dfs strategy for large complex dimensions m>=2048. - */ - -#include <math.h> - -#include "../commons.h" -#include "../ext/neon_accel/macrof.h" -#include "../ext/neon_accel/macrofx4.h" - -void fill_reim_fft16_omegas_neon(const double entry_pwr, double** omg) { - const double j_pow = 1. / 8.; - const double k_pow = 1. / 16.; - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - const double pin_4 = entry_pwr / 8.; - const double pin_8 = entry_pwr / 16.; - // 0 and 1 are real and imag of om - (*omg)[0] = cos(2. * M_PI * pin); - (*omg)[1] = sin(2. * M_PI * pin); - // 2 and 3 are real and imag of om^1/2 - (*omg)[2] = cos(2. * M_PI * (pin_2)); - (*omg)[3] = sin(2. * M_PI * (pin_2)); - // (4,5) and (6,7) are real and imag of om^1/4 and j.om^1/4 - (*omg)[4] = cos(2. * M_PI * (pin_4)); - (*omg)[5] = sin(2. * M_PI * (pin_4)); - (*omg)[6] = cos(2. * M_PI * (pin_4 + j_pow)); - (*omg)[7] = sin(2. * M_PI * (pin_4 + j_pow)); - // ((8,9,10,11),(12,13,14,15)) are 4 reals then 4 imag of om^1/8*(1,k,j,kj) - (*omg)[8] = cos(2. * M_PI * (pin_8)); - (*omg)[10] = cos(2. * M_PI * (pin_8 + j_pow)); - (*omg)[12] = cos(2. * M_PI * (pin_8 + k_pow)); - (*omg)[14] = cos(2. * M_PI * (pin_8 + j_pow + k_pow)); - (*omg)[9] = sin(2. * M_PI * (pin_8)); - (*omg)[11] = sin(2. * M_PI * (pin_8 + j_pow)); - (*omg)[13] = sin(2. * M_PI * (pin_8 + k_pow)); - (*omg)[15] = sin(2. * M_PI * (pin_8 + j_pow + k_pow)); - *omg += 16; -} - -EXPORT void reim_fft16_neon(double* dre, double* dim, const void* omega) { - const double* pom = omega; - // Total SIMD register: 28 = 24 + 4 - float64x2x2_t s_re_im; // 2 - float64x2x4_t x_re, x_im, y_re, y_im, t_re, t_im, v_re, v_im; // 32 - - { - /* - Level 2 - ( 8, 24) * ( 0, 1) - ( 9, 25) * ( 0, 1) - ( 10, 26) * ( 0, 1) - ( 11, 27) * ( 0, 1) - ( 12, 28) * ( 0, 1) - ( 13, 29) * ( 0, 1) - ( 14, 30) * ( 0, 1) - ( 15, 31) * ( 0, 1) - - ( 8, 24) = ( 0, 16) - @ - ( 9, 25) = ( 1, 17) - @ - ( 10, 26) = ( 2, 18) - @ - ( 11, 27) = ( 3, 19) - @ - ( 12, 28) = ( 4, 20) - @ - ( 13, 29) = ( 5, 21) - @ - ( 14, 30) = ( 6, 22) - @ - ( 15, 31) = ( 7, 23) - @ - - ( 0, 16) = ( 0, 16) + @ - ( 1, 17) = ( 1, 17) + @ - ( 2, 18) = ( 2, 18) + @ - ( 3, 19) = ( 3, 19) + @ - ( 4, 20) = ( 4, 20) + @ - ( 5, 21) = ( 5, 21) + @ - ( 6, 22) = ( 6, 22) + @ - ( 7, 23) = ( 7, 23) + @ - */ - vload(s_re_im.val[0], pom); - - vloadx4(y_re, dre + 8); - vloadx4(y_im, dim + 8); - - FWD_TOP_LANEx4(v_re, v_im, y_re, y_im, s_re_im.val[0]); - - vloadx4(x_re, dre); - vloadx4(x_im, dim); - - FWD_BOTx4(x_re, x_im, y_re, y_im, v_re, v_im); - - // vstorex4(dre, x_re); - // vstorex4(dim, x_im); - // vstorex4(dre + 8, y_re); - // vstorex4(dim + 8, y_im); - // return; - } - { - /* - Level 3 - - ( 4, 20) * ( 0, 1) - ( 5, 21) * ( 0, 1) - ( 6, 22) * ( 0, 1) - ( 7, 23) * ( 0, 1) - - ( 4, 20) = ( 0, 16) - @ - ( 5, 21) = ( 1, 17) - @ - ( 6, 22) = ( 2, 18) - @ - ( 7, 23) = ( 3, 19) - @ - - ( 0, 16) = ( 0, 16) + @ - ( 1, 17) = ( 1, 17) + @ - ( 2, 18) = ( 2, 18) + @ - ( 3, 19) = ( 3, 19) + @ - - ( 12, 28) * ( 0, 1) - ( 13, 29) * ( 0, 1) - ( 14, 30) * ( 0, 1) - ( 15, 31) * ( 0, 1) - - ( 12, 28) = ( 8, 24) - j@ - ( 13, 29) = ( 9, 25) - j@ - ( 14, 30) = ( 10, 26) - j@ - ( 15, 31) = ( 11, 27) - j@ - - ( 8, 24) = ( 8, 24) + j@ - ( 9, 25) = ( 9, 25) + j@ - ( 10, 26) = ( 10, 26) + j@ - ( 11, 27) = ( 11, 27) + j@ - */ - - // vloadx4(y_re, dre + 8); - // vloadx4(y_im, dim + 8); - // vloadx4(x_re, dre); - // vloadx4(x_im, dim); - - vload(s_re_im.val[0], pom + 2); - - FWD_TOP_LANE(t_re.val[0], t_im.val[0], x_re.val[2], x_im.val[2], s_re_im.val[0]); - FWD_TOP_LANE(t_re.val[1], t_im.val[1], x_re.val[3], x_im.val[3], s_re_im.val[0]); - FWD_TOP_LANE(t_re.val[2], t_im.val[2], y_re.val[2], y_im.val[2], s_re_im.val[0]); - FWD_TOP_LANE(t_re.val[3], t_im.val[3], y_re.val[3], y_im.val[3], s_re_im.val[0]); - - FWD_BOT(x_re.val[0], x_im.val[0], x_re.val[2], x_im.val[2], t_re.val[0], t_im.val[0]); - FWD_BOT(x_re.val[1], x_im.val[1], x_re.val[3], x_im.val[3], t_re.val[1], t_im.val[1]); - FWD_BOTJ(y_re.val[0], y_im.val[0], y_re.val[2], y_im.val[2], t_re.val[2], t_im.val[2]); - FWD_BOTJ(y_re.val[1], y_im.val[1], y_re.val[3], y_im.val[3], t_re.val[3], t_im.val[3]); - - // vstorex4(dre, x_re); - // vstorex4(dim, x_im); - // vstorex4(dre + 8, y_re); - // vstorex4(dim + 8, y_im); - // return; - } - { - /* - Level 4 - - ( 2, 18) * ( 0, 1) - ( 3, 19) * ( 0, 1) - ( 6, 22) * ( 0, 1) - ( 7, 23) * ( 0, 1) - - ( 2, 18) = ( 0, 16) - @ - ( 3, 19) = ( 1, 17) - @ - ( 0, 16) = ( 0, 16) + @ - ( 1, 17) = ( 1, 17) + @ - - ( 6, 22) = ( 4, 20) - j@ - ( 7, 23) = ( 5, 21) - j@ - ( 4, 20) = ( 4, 20) + j@ - ( 5, 21) = ( 5, 21) + j@ - - ( 10, 26) * ( 2, 3) - ( 11, 27) * ( 2, 3) - ( 14, 30) * ( 2, 3) - ( 15, 31) * ( 2, 3) - - ( 10, 26) = ( 8, 24) - @ - ( 11, 27) = ( 9, 25) - @ - ( 8, 24) = ( 8, 24) + @ - ( 9, 25) = ( 9, 25) + @ - - ( 14, 30) = ( 12, 28) - j@ - ( 15, 31) = ( 13, 29) - j@ - ( 12, 28) = ( 12, 28) + j@ - ( 13, 29) = ( 13, 29) + j@ - */ - // vloadx4(y_re, dre + 8); - // vloadx4(y_im, dim + 8); - // vloadx4(x_re, dre); - // vloadx4(x_im, dim); - - vloadx2(s_re_im, pom + 4); - - FWD_TOP_LANE(t_re.val[0], t_im.val[0], x_re.val[1], x_im.val[1], s_re_im.val[0]); - FWD_TOP_LANE(t_re.val[1], t_im.val[1], x_re.val[3], x_im.val[3], s_re_im.val[0]); - FWD_TOP_LANE(t_re.val[2], t_im.val[2], y_re.val[1], y_im.val[1], s_re_im.val[1]); - FWD_TOP_LANE(t_re.val[3], t_im.val[3], y_re.val[3], y_im.val[3], s_re_im.val[1]); - - FWD_BOT(x_re.val[0], x_im.val[0], x_re.val[1], x_im.val[1], t_re.val[0], t_im.val[0]); - FWD_BOTJ(x_re.val[2], x_im.val[2], x_re.val[3], x_im.val[3], t_re.val[1], t_im.val[1]); - FWD_BOT(y_re.val[0], y_im.val[0], y_re.val[1], y_im.val[1], t_re.val[2], t_im.val[2]); - FWD_BOTJ(y_re.val[2], y_im.val[2], y_re.val[3], y_im.val[3], t_re.val[3], t_im.val[3]); - - // vstorex4(dre, x_re); - // vstorex4(dim, x_im); - // vstorex4(dre + 8, y_re); - // vstorex4(dim + 8, y_im); - // return; - } - { - /* - Level 5 - - ( 1, 17) * ( 0, 1) - ( 5, 21) * ( 2, 3) - ------ - ( 1, 17) = ( 0, 16) - @ - ( 5, 21) = ( 4, 20) - @ - ( 0, 16) = ( 0, 16) + @ - ( 4, 20) = ( 4, 20) + @ - - ( 3, 19) * ( 0, 1) - ( 7, 23) * ( 2, 3) - ------ - ( 3, 19) = ( 2, 18) - j@ - ( 7, 23) = ( 6, 22) - j@ - ( 2, 18) = ( 2, 18) + j@ - ( 6, 22) = ( 6, 22) + j@ - - ( 9, 25) * ( 4, 5) - ( 13, 29) * ( 6, 7) - ------ - ( 9, 25) = ( 8, 24) - @ - ( 13, 29) = ( 12, 28) - @ - ( 8, 24) = ( 8, 24) + @ - ( 12, 28) = ( 12, 28) + @ - - ( 11, 27) * ( 4, 5) - ( 15, 31) * ( 6, 7) - ------ - ( 11, 27) = ( 10, 26) - j@ - ( 15, 31) = ( 14, 30) - j@ - ( 10, 26) = ( 10, 26) + j@ - ( 14, 30) = ( 14, 30) + j@ - - before transpose_f64 - x_re: 0, 1 | 2, 3 | 4, 5 | 6, 7 - y_re: 8, 9 | 10, 11 | 12, 13 | 14, 15 - after transpose_f64 - x_re: 0, 4 | 2, 6 | 1, 5 | 3, 7 - y_re: 8, 12| 9, 13| 10, 14 | 11, 15 - after swap - x_re: 0, 4 | 1, 5 | 2, 6 | 3, 7 - y_re: 8, 12| 10, 14 | 9, 13| 11, 15 - */ - - // vloadx4(y_re, dre + 8); - // vloadx4(y_im, dim + 8); - // vloadx4(x_re, dre); - // vloadx4(x_im, dim); - - transpose_f64(x_re, x_re, v_re, 0, 2, 0); - transpose_f64(x_re, x_re, v_re, 1, 3, 1); - transpose_f64(x_im, x_im, v_im, 0, 2, 0); - transpose_f64(x_im, x_im, v_im, 1, 3, 1); - - v_re.val[0] = x_re.val[2]; - x_re.val[2] = x_re.val[1]; - x_re.val[1] = v_re.val[0]; - - v_im.val[0] = x_im.val[2]; - x_im.val[2] = x_im.val[1]; - x_im.val[1] = v_im.val[0]; - - transpose_f64(y_re, y_re, v_re, 0, 2, 2); - transpose_f64(y_re, y_re, v_re, 1, 3, 3); - transpose_f64(y_im, y_im, v_im, 0, 2, 2); - transpose_f64(y_im, y_im, v_im, 1, 3, 3); - - v_re.val[0] = y_re.val[2]; - y_re.val[2] = y_re.val[1]; - y_re.val[1] = v_re.val[0]; - - v_im.val[0] = y_im.val[2]; - y_im.val[2] = y_im.val[1]; - y_im.val[1] = v_im.val[0]; - - // double pom8[] = {pom[8], pom[12], pom[9], pom[13]}; - vload2(s_re_im, pom + 8); - // vload2(s_re_im, pom8); - - FWD_TOP(t_re.val[0], t_im.val[0], x_re.val[1], x_im.val[1], s_re_im.val[0], s_re_im.val[1]); - FWD_TOP(t_re.val[1], t_im.val[1], x_re.val[3], x_im.val[3], s_re_im.val[0], s_re_im.val[1]); - - // double pom12[] = {pom[10], pom[14], pom[11], pom[15]}; - vload2(s_re_im, pom + 12); - // vload2(s_re_im, pom12); - - FWD_TOP(t_re.val[2], t_im.val[2], y_re.val[1], y_im.val[1], s_re_im.val[0], s_re_im.val[1]); - FWD_TOP(t_re.val[3], t_im.val[3], y_re.val[3], y_im.val[3], s_re_im.val[0], s_re_im.val[1]); - - FWD_BOT(x_re.val[0], x_im.val[0], x_re.val[1], x_im.val[1], t_re.val[0], t_im.val[0]); - FWD_BOTJ(x_re.val[2], x_im.val[2], x_re.val[3], x_im.val[3], t_re.val[1], t_im.val[1]); - - vstore4(dre, x_re); - vstore4(dim, x_im); - - FWD_BOT(y_re.val[0], y_im.val[0], y_re.val[1], y_im.val[1], t_re.val[2], t_im.val[2]); - FWD_BOTJ(y_re.val[2], y_im.val[2], y_re.val[3], y_im.val[3], t_re.val[3], t_im.val[3]); - - vstore4(dre + 8, y_re); - vstore4(dim + 8, y_im); - } -} - -void reim_twiddle_fft_neon(uint64_t h, double* re, double* im, double om[2]) { - // Total SIMD register: 28 = 24 + 4 - if (h < 8) abort(); // bug - float64x2_t s_re_im; // 2 - float64x2x4_t x_re, x_im, y_re, y_im, v_re, v_im; // 32 - vload(s_re_im, om); - for (uint64_t blk = 0; blk < h; blk += 8) { - double* dre = re + blk; - double* dim = im + blk; - vloadx4(y_re, dre + h); - vloadx4(y_im, dim + h); - FWD_TOP_LANEx4(v_re, v_im, y_re, y_im, s_re_im); - vloadx4(x_re, dre); - vloadx4(x_im, dim); - FWD_BOTx4(x_re, x_im, y_re, y_im, v_re, v_im); - vstorex4(dre, x_re); - vstorex4(dim, x_im); - vstorex4(dre + h, y_re); - vstorex4(dim + h, y_im); - } -} - -void reim_ctwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim); -// i (omre + i omim) = -omim + i omre -void reim_citwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim); - -void reim_bitwiddle_fft_neon(uint64_t h, double* re, double* im, double om[4]) { - // Total SIMD register: 28 = 24 + 4 - if (h < 4) abort(); // bug - double* r0 = re; - double* r1 = re + h; - double* r2 = re + 2 * h; - double* r3 = re + 3 * h; - double* i0 = im; - double* i1 = im + h; - double* i2 = im + 2 * h; - double* i3 = im + 3 * h; - float64x2x2_t s_re_im; // 2 - float64x2x4_t v_re, v_im; // 2 - float64x2x2_t x0_re, x0_im, x1_re, x1_im; // 32 - float64x2x2_t x2_re, x2_im, x3_re, x3_im; // 32 - vloadx2(s_re_im, om); - for (uint64_t blk = 0; blk < h; blk += 4) { - { - vloadx2(x2_re, r2 + blk); - vloadx2(x3_re, r3 + blk); - vloadx2(x2_im, i2 + blk); - vloadx2(x3_im, i3 + blk); - FWD_TOP_LANE(v_re.val[0], v_im.val[0], x2_re.val[0], x2_im.val[0], s_re_im.val[0]); - FWD_TOP_LANE(v_re.val[1], v_im.val[1], x2_re.val[1], x2_im.val[1], s_re_im.val[0]); - FWD_TOP_LANE(v_re.val[2], v_im.val[2], x3_re.val[0], x3_im.val[0], s_re_im.val[0]); - FWD_TOP_LANE(v_re.val[3], v_im.val[3], x3_re.val[1], x3_im.val[1], s_re_im.val[0]); - vloadx2(x0_re, r0 + blk); - vloadx2(x1_re, r1 + blk); - vloadx2(x0_im, i0 + blk); - vloadx2(x1_im, i1 + blk); - FWD_BOT(x0_re.val[0], x0_im.val[0], x2_re.val[0], x2_im.val[0], v_re.val[0], v_im.val[0]); - FWD_BOT(x0_re.val[1], x0_im.val[1], x2_re.val[1], x2_im.val[1], v_re.val[1], v_im.val[1]); - FWD_BOT(x1_re.val[0], x1_im.val[0], x3_re.val[0], x3_im.val[0], v_re.val[2], v_im.val[2]); - FWD_BOT(x1_re.val[1], x1_im.val[1], x3_re.val[1], x3_im.val[1], v_re.val[3], v_im.val[3]); - } - { - FWD_TOP_LANE(v_re.val[0], v_im.val[0], x1_re.val[0], x1_im.val[0], s_re_im.val[1]); - FWD_TOP_LANE(v_re.val[1], v_im.val[1], x1_re.val[1], x1_im.val[1], s_re_im.val[1]); - FWD_TOP_LANE(v_re.val[2], v_im.val[2], x3_re.val[0], x3_im.val[0], s_re_im.val[1]); - FWD_TOP_LANE(v_re.val[3], v_im.val[3], x3_re.val[1], x3_im.val[1], s_re_im.val[1]); - FWD_BOT(x0_re.val[0], x0_im.val[0], x1_re.val[0], x1_im.val[0], v_re.val[0], v_im.val[0]); - FWD_BOT(x0_re.val[1], x0_im.val[1], x1_re.val[1], x1_im.val[1], v_re.val[1], v_im.val[1]); - FWD_BOTJ(x2_re.val[0], x2_im.val[0], x3_re.val[0], x3_im.val[0], v_re.val[2], v_im.val[2]); - FWD_BOTJ(x2_re.val[1], x2_im.val[1], x3_re.val[1], x3_im.val[1], v_re.val[3], v_im.val[3]); - vstorex2(r0 + blk, x0_re); - vstorex2(r1 + blk, x1_re); - vstorex2(r2 + blk, x2_re); - vstorex2(r3 + blk, x3_re); - vstorex2(i0 + blk, x0_im); - vstorex2(i1 + blk, x1_im); - vstorex2(i2 + blk, x2_im); - vstorex2(i3 + blk, x3_im); - } - } -} - -#if 0 -static void ZfN(iFFT_log2)(fpr *f) -{ - /* - y_re: 1 = (2 - 3) * 5 + (0 - 1) * 4 - y_im: 3 = (2 - 3) * 4 - (0 - 1) * 5 - x_re: 0 = 0 + 1 - x_im: 2 = 2 + 3 - - Turn out this vectorize code is too short to be executed, - the scalar version is consistently faster - - float64x2x2_t tmp; - float64x2_t v, s, t; - - // 0: 0, 2 - // 1: 1, 3 - - vload2(tmp, &f[0]); - vload(s, &fpr_gm_tab[4]); - - vfsub(v, tmp.val[0], tmp.val[1]); - vfadd(tmp.val[0], tmp.val[0], tmp.val[1]); - - // y_im: 3 = (2 - 3) * 4 - (0 - 1) * 5 - // y_re: 1 = (2 - 3) * 5 + (0 - 1) * 4 - vswap(t, v); - - vfmul_lane(tmp.val[1], s, t, 0); - vfcmla_90(tmp.val[1], t, s); - - vfmuln(tmp.val[0], tmp.val[0], 0.5); - vfmuln(tmp.val[1], tmp.val[1], 0.5); - - vswap(tmp.val[1], tmp.val[1]); - - vstore2(&f[0], tmp); - */ - - fpr x_re, x_im, y_re, y_im, s; - x_re = f[0]; - y_re = f[1]; - x_im = f[2]; - y_im = f[3]; - s = fpr_tab_log2[0] * 0.5; - - f[0] = (x_re + y_re) * 0.5; - f[2] = (x_im + y_im) * 0.5; - - x_re = (x_re - y_re) * s; - x_im = (x_im - y_im) * s; - - f[1] = x_im + x_re; - f[3] = x_im - x_re; -} - -static void ZfN(iFFT_log3)(fpr *f) -{ - /* - * Total instructions: 27 - y_re: 1 = (4 - 5) * 9 + (0 - 1) * 8 - y_re: 3 = (6 - 7) * 11 + (2 - 3) * 10 - y_im: 5 = (4 - 5) * 8 - (0 - 1) * 9 - y_im: 7 = (6 - 7) * 10 - (2 - 3) * 11 - x_re: 0 = 0 + 1 - x_re: 2 = 2 + 3 - x_im: 4 = 4 + 5 - x_im: 6 = 6 + 7 - */ - // 0: 0, 2 - 0: 0, 4 - // 1: 1, 3 - 1: 1, 5 - // 2: 4, 6 - 2: 2, 6 - // 3: 5, 7 - 3: 3, 7 - float64x2x4_t tmp; - float64x2x2_t x_re_im, y_re_im, v, s_re_im; - - vload2(x_re_im, &f[0]); - vload2(y_re_im, &f[4]); - - vfsub(v.val[0], x_re_im.val[0], x_re_im.val[1]); - vfsub(v.val[1], y_re_im.val[0], y_re_im.val[1]); - vfadd(x_re_im.val[0], x_re_im.val[0], x_re_im.val[1]); - vfadd(x_re_im.val[1], y_re_im.val[0], y_re_im.val[1]); - - // 0: 8, 10 - // 1: 9, 11 - vload2(s_re_im, &fpr_tab_log3[0]); - - vfmul(y_re_im.val[0], v.val[1], s_re_im.val[1]); - vfmla(y_re_im.val[0], y_re_im.val[0], v.val[0], s_re_im.val[0]); - vfmul(y_re_im.val[1], v.val[1], s_re_im.val[0]); - vfmls(y_re_im.val[1], y_re_im.val[1], v.val[0], s_re_im.val[1]); - - // x: 0,2 | 4,6 - // y: 1,3 | 5,7 - tmp.val[0] = vtrn1q_f64(x_re_im.val[0], y_re_im.val[0]); - tmp.val[1] = vtrn2q_f64(x_re_im.val[0], y_re_im.val[0]); - tmp.val[2] = vtrn1q_f64(x_re_im.val[1], y_re_im.val[1]); - tmp.val[3] = vtrn2q_f64(x_re_im.val[1], y_re_im.val[1]); - // tmp: 0,1 | 2,3 | 4,5 | 6,7 - /* - y_re: 2 = (4 - 6) * 4 + (0 - 2) * 4 - y_re: 3 = (5 - 7) * 4 + (1 - 3) * 4 - y_im: 6 = (4 - 6) * 4 - (0 - 2) * 4 - y_im: 7 = (5 - 7) * 4 - (1 - 3) * 4 - x_re: 0 = 0 + 2 - x_re: 1 = 1 + 3 - x_im: 4 = 4 + 6 - x_im: 5 = 5 + 7 - */ - s_re_im.val[0] = vld1q_dup_f64(&fpr_tab_log2[0]); - - vfadd(x_re_im.val[0], tmp.val[0], tmp.val[1]); - vfadd(x_re_im.val[1], tmp.val[2], tmp.val[3]); - vfsub(v.val[0], tmp.val[0], tmp.val[1]); - vfsub(v.val[1], tmp.val[2], tmp.val[3]); - - vfmuln(tmp.val[0], x_re_im.val[0], 0.25); - vfmuln(tmp.val[2], x_re_im.val[1], 0.25); - - vfmuln(s_re_im.val[0], s_re_im.val[0], 0.25); - - vfmul(y_re_im.val[0], v.val[0], s_re_im.val[0]); - vfmul(y_re_im.val[1], v.val[1], s_re_im.val[0]); - - vfadd(tmp.val[1], y_re_im.val[1], y_re_im.val[0]); - vfsub(tmp.val[3], y_re_im.val[1], y_re_im.val[0]); - - vstorex4(&f[0], tmp); -} - -static void ZfN(iFFT_log4)(fpr *f) -{ - /* - * ( 0, 8) - ( 1, 9) - * ( 4, 12) - ( 5, 13) - * - * ( 0, 8) + ( 1, 9) - * ( 4, 12) + ( 5, 13) - * - * ( 3, 11) - ( 2, 10) - * ( 7, 15) - ( 6, 14) - * - * ( 2, 10) + ( 3, 11) - * ( 6, 14) + ( 7, 15) - * - * ( 1, 9) = @ * ( 0, 1) - * ( 5, 13) = @ * ( 2, 3) - * - * ( 3, 11) = j@ * ( 0, 1) - * ( 7, 15) = j@ * ( 2, 3) - */ - - float64x2x4_t re, im, t; - float64x2x2_t t_re, t_im, s_re_im; - - vload4(re, &f[0]); - vload4(im, &f[8]); - - INV_TOPJ (t_re.val[0], t_im.val[0], re.val[0], im.val[0], re.val[1], im.val[1]); - INV_TOPJm(t_re.val[1], t_im.val[1], re.val[2], im.val[2], re.val[3], im.val[3]); - - vload2(s_re_im, &fpr_tab_log4[0]); - - INV_BOTJ (re.val[1], im.val[1], t_re.val[0], t_im.val[0], s_re_im.val[0], s_re_im.val[1]); - INV_BOTJm(re.val[3], im.val[3], t_re.val[1], t_im.val[1], s_re_im.val[0], s_re_im.val[1]); - - /* - * ( 0, 8) - ( 2, 10) - * ( 1, 9) - ( 3, 11) - * - * ( 0, 8) + ( 2, 10) - * ( 1, 9) + ( 3, 11) - * - * ( 2, 10) = @ * ( 0, 1) - * ( 3, 11) = @ * ( 0, 1) - * - * ( 6, 14) - ( 4, 12) - * ( 7, 15) - ( 5, 13) - * - * ( 4, 12) + ( 6, 14) - * ( 5, 13) + ( 7, 15) - * - * ( 6, 14) = j@ * ( 0, 1) - * ( 7, 15) = j@ * ( 0, 1) - */ - - // re: 0, 4 | 1, 5 | 2, 6 | 3, 7 - // im: 8, 12| 9, 13|10, 14|11, 15 - - transpose_f64(re, re, t, 0, 1, 0); - transpose_f64(re, re, t, 2, 3, 1); - transpose_f64(im, im, t, 0, 1, 2); - transpose_f64(im, im, t, 2, 3, 3); - - // re: 0, 1 | 4, 5 | 2, 3 | 6, 7 - // im: 8, 9 | 12, 13|10, 11| 14, 15 - t.val[0] = re.val[1]; - re.val[1] = re.val[2]; - re.val[2] = t.val[0]; - - t.val[1] = im.val[1]; - im.val[1] = im.val[2]; - im.val[2] = t.val[1]; - - // re: 0, 1 | 2, 3| 4, 5 | 6, 7 - // im: 8, 9 | 10, 11| 12, 13| 14, 15 - - INV_TOPJ (t_re.val[0], t_im.val[0], re.val[0], im.val[0], re.val[1], im.val[1]); - INV_TOPJm(t_re.val[1], t_im.val[1], re.val[2], im.val[2], re.val[3], im.val[3]); - - vload(s_re_im.val[0], &fpr_tab_log3[0]); - - INV_BOTJ_LANE (re.val[1], im.val[1], t_re.val[0], t_im.val[0], s_re_im.val[0]); - INV_BOTJm_LANE(re.val[3], im.val[3], t_re.val[1], t_im.val[1], s_re_im.val[0]); - - /* - * ( 0, 8) - ( 4, 12) - * ( 1, 9) - ( 5, 13) - * ( 0, 8) + ( 4, 12) - * ( 1, 9) + ( 5, 13) - * - * ( 2, 10) - ( 6, 14) - * ( 3, 11) - ( 7, 15) - * ( 2, 10) + ( 6, 14) - * ( 3, 11) + ( 7, 15) - * - * ( 4, 12) = @ * ( 0, 1) - * ( 5, 13) = @ * ( 0, 1) - * - * ( 6, 14) = @ * ( 0, 1) - * ( 7, 15) = @ * ( 0, 1) - */ - - INV_TOPJ(t_re.val[0], t_im.val[0], re.val[0], im.val[0], re.val[2], im.val[2]); - INV_TOPJ(t_re.val[1], t_im.val[1], re.val[1], im.val[1], re.val[3], im.val[3]); - - vfmuln(re.val[0], re.val[0], 0.12500000000); - vfmuln(re.val[1], re.val[1], 0.12500000000); - vfmuln(im.val[0], im.val[0], 0.12500000000); - vfmuln(im.val[1], im.val[1], 0.12500000000); - - s_re_im.val[0] = vld1q_dup_f64(&fpr_tab_log2[0]); - - vfmuln(s_re_im.val[0], s_re_im.val[0], 0.12500000000); - - vfmul(t_re.val[0], t_re.val[0], s_re_im.val[0]); - vfmul(t_re.val[1], t_re.val[1], s_re_im.val[0]); - vfmul(t_im.val[0], t_im.val[0], s_re_im.val[0]); - vfmul(t_im.val[1], t_im.val[1], s_re_im.val[0]); - - vfsub(im.val[2], t_im.val[0], t_re.val[0]); - vfsub(im.val[3], t_im.val[1], t_re.val[1]); - vfadd(re.val[2], t_im.val[0], t_re.val[0]); - vfadd(re.val[3], t_im.val[1], t_re.val[1]); - - vstorex4(&f[0], re); - vstorex4(&f[8], im); -} - -static - void ZfN(iFFT_log5)(fpr *f, const unsigned logn, const unsigned last) -{ - // Total SIMD register: 26 = 24 + 2 - float64x2x4_t x_re, x_im, y_re, y_im, t_re, t_im; // 24 - float64x2x2_t s_re_im; // 2 - const unsigned n = 1 << logn; - const unsigned hn = n >> 1; - - int level = logn; - const fpr *fpr_tab5 = fpr_table[level--], - *fpr_tab4 = fpr_table[level--], - *fpr_tab3 = fpr_table[level--], - *fpr_tab2 = fpr_table[level]; - int k2 = 0, k3 = 0, k4 = 0, k5 = 0; - - for (unsigned j = 0; j < hn; j += 16) - { - /* - * ( 0, 16) - ( 1, 17) - * ( 4, 20) - ( 5, 21) - * ( 0, 16) + ( 1, 17) - * ( 4, 20) + ( 5, 21) - * ( 1, 17) = @ * ( 0, 1) - * ( 5, 21) = @ * ( 2, 3) - * - * ( 2, 18) - ( 3, 19) - * ( 6, 22) - ( 7, 23) - * ( 2, 18) + ( 3, 19) - * ( 6, 22) + ( 7, 23) - * ( 3, 19) = j@ * ( 0, 1) - * ( 7, 23) = j@ * ( 2, 3) - * - * ( 8, 24) - ( 9, 25) - * ( 12, 28) - ( 13, 29) - * ( 8, 24) + ( 9, 25) - * ( 12, 28) + ( 13, 29) - * ( 9, 25) = @ * ( 4, 5) - * ( 13, 29) = @ * ( 6, 7) - * - * ( 10, 26) - ( 11, 27) - * ( 14, 30) - ( 15, 31) - * ( 10, 26) + ( 11, 27) - * ( 14, 30) + ( 15, 31) - * ( 11, 27) = j@ * ( 4, 5) - * ( 15, 31) = j@ * ( 6, 7) - */ - - vload4(x_re, &f[j]); - vload4(x_im, &f[j + hn]); - - INV_TOPJ(t_re.val[0], t_im.val[0], x_re.val[0], x_im.val[0], x_re.val[1], x_im.val[1]); - INV_TOPJm(t_re.val[2], t_im.val[2], x_re.val[2], x_im.val[2], x_re.val[3], x_im.val[3]); - - vload4(y_re, &f[j + 8]); - vload4(y_im, &f[j + 8 + hn]) - - INV_TOPJ(t_re.val[1], t_im.val[1], y_re.val[0], y_im.val[0], y_re.val[1], y_im.val[1]); - INV_TOPJm(t_re.val[3], t_im.val[3], y_re.val[2], y_im.val[2], y_re.val[3], y_im.val[3]); - - vload2(s_re_im, &fpr_tab5[k5]); - k5 += 4; - - INV_BOTJ (x_re.val[1], x_im.val[1], t_re.val[0], t_im.val[0], s_re_im.val[0], s_re_im.val[1]); - INV_BOTJm(x_re.val[3], x_im.val[3], t_re.val[2], t_im.val[2], s_re_im.val[0], s_re_im.val[1]); - - vload2(s_re_im, &fpr_tab5[k5]); - k5 += 4; - - INV_BOTJ (y_re.val[1], y_im.val[1], t_re.val[1], t_im.val[1], s_re_im.val[0], s_re_im.val[1]); - INV_BOTJm(y_re.val[3], y_im.val[3], t_re.val[3], t_im.val[3], s_re_im.val[0], s_re_im.val[1]); - - - // x_re: 0, 4 | 1, 5 | 2, 6 | 3, 7 - // y_re: 8, 12| 9, 13|10, 14|11, 15 - - transpose_f64(x_re, x_re, t_re, 0, 1, 0); - transpose_f64(x_re, x_re, t_re, 2, 3, 1); - transpose_f64(y_re, y_re, t_re, 0, 1, 2); - transpose_f64(y_re, y_re, t_re, 2, 3, 3); - - transpose_f64(x_im, x_im, t_im, 0, 1, 0); - transpose_f64(x_im, x_im, t_im, 2, 3, 1); - transpose_f64(y_im, y_im, t_im, 0, 1, 2); - transpose_f64(y_im, y_im, t_im, 2, 3, 3); - - // x_re: 0, 1 | 4, 5 | 2, 3 | 6, 7 - // y_re: 8, 9 | 12,13|10,11 |14, 15 - - t_re.val[0] = x_re.val[1]; - x_re.val[1] = x_re.val[2]; - x_re.val[2] = t_re.val[0]; - - t_re.val[1] = y_re.val[1]; - y_re.val[1] = y_re.val[2]; - y_re.val[2] = t_re.val[1]; - - - t_im.val[0] = x_im.val[1]; - x_im.val[1] = x_im.val[2]; - x_im.val[2] = t_im.val[0]; - - t_im.val[1] = y_im.val[1]; - y_im.val[1] = y_im.val[2]; - y_im.val[2] = t_im.val[1]; - // x_re: 0, 1 | 2, 3| 4, 5 | 6, 7 - // y_re: 8, 9 | 10, 11| 12, 13| 14, 15 - - /* - * ( 0, 16) - ( 2, 18) - * ( 1, 17) - ( 3, 19) - * ( 0, 16) + ( 2, 18) - * ( 1, 17) + ( 3, 19) - * ( 2, 18) = @ * ( 0, 1) - * ( 3, 19) = @ * ( 0, 1) - * - * ( 4, 20) - ( 6, 22) - * ( 5, 21) - ( 7, 23) - * ( 4, 20) + ( 6, 22) - * ( 5, 21) + ( 7, 23) - * ( 6, 22) = j@ * ( 0, 1) - * ( 7, 23) = j@ * ( 0, 1) - * - * ( 8, 24) - ( 10, 26) - * ( 9, 25) - ( 11, 27) - * ( 8, 24) + ( 10, 26) - * ( 9, 25) + ( 11, 27) - * ( 10, 26) = @ * ( 2, 3) - * ( 11, 27) = @ * ( 2, 3) - * - * ( 12, 28) - ( 14, 30) - * ( 13, 29) - ( 15, 31) - * ( 12, 28) + ( 14, 30) - * ( 13, 29) + ( 15, 31) - * ( 14, 30) = j@ * ( 2, 3) - * ( 15, 31) = j@ * ( 2, 3) - */ - - INV_TOPJ (t_re.val[0], t_im.val[0], x_re.val[0], x_im.val[0], x_re.val[1], x_im.val[1]); - INV_TOPJm(t_re.val[1], t_im.val[1], x_re.val[2], x_im.val[2], x_re.val[3], x_im.val[3]); - - INV_TOPJ (t_re.val[2], t_im.val[2], y_re.val[0], y_im.val[0], y_re.val[1], y_im.val[1]); - INV_TOPJm(t_re.val[3], t_im.val[3], y_re.val[2], y_im.val[2], y_re.val[3], y_im.val[3]); - - vloadx2(s_re_im, &fpr_tab4[k4]); - k4 += 4; - - INV_BOTJ_LANE (x_re.val[1], x_im.val[1], t_re.val[0], t_im.val[0], s_re_im.val[0]); - INV_BOTJm_LANE(x_re.val[3], x_im.val[3], t_re.val[1], t_im.val[1], s_re_im.val[0]); - - INV_BOTJ_LANE (y_re.val[1], y_im.val[1], t_re.val[2], t_im.val[2], s_re_im.val[1]); - INV_BOTJm_LANE(y_re.val[3], y_im.val[3], t_re.val[3], t_im.val[3], s_re_im.val[1]); - - /* - * ( 0, 16) - ( 4, 20) - * ( 1, 17) - ( 5, 21) - * ( 0, 16) + ( 4, 20) - * ( 1, 17) + ( 5, 21) - * ( 4, 20) = @ * ( 0, 1) - * ( 5, 21) = @ * ( 0, 1) - * - * ( 2, 18) - ( 6, 22) - * ( 3, 19) - ( 7, 23) - * ( 2, 18) + ( 6, 22) - * ( 3, 19) + ( 7, 23) - * ( 6, 22) = @ * ( 0, 1) - * ( 7, 23) = @ * ( 0, 1) - * - * ( 8, 24) - ( 12, 28) - * ( 9, 25) - ( 13, 29) - * ( 8, 24) + ( 12, 28) - * ( 9, 25) + ( 13, 29) - * ( 12, 28) = j@ * ( 0, 1) - * ( 13, 29) = j@ * ( 0, 1) - * - * ( 10, 26) - ( 14, 30) - * ( 11, 27) - ( 15, 31) - * ( 10, 26) + ( 14, 30) - * ( 11, 27) + ( 15, 31) - * ( 14, 30) = j@ * ( 0, 1) - * ( 15, 31) = j@ * ( 0, 1) - */ - - INV_TOPJ (t_re.val[0], t_im.val[0], x_re.val[0], x_im.val[0], x_re.val[2], x_im.val[2]); - INV_TOPJ (t_re.val[1], t_im.val[1], x_re.val[1], x_im.val[1], x_re.val[3], x_im.val[3]); - - INV_TOPJm(t_re.val[2], t_im.val[2], y_re.val[0], y_im.val[0], y_re.val[2], y_im.val[2]); - INV_TOPJm(t_re.val[3], t_im.val[3], y_re.val[1], y_im.val[1], y_re.val[3], y_im.val[3]); - - vload(s_re_im.val[0], &fpr_tab3[k3]); - k3 += 2; - - INV_BOTJ_LANE(x_re.val[2], x_im.val[2], t_re.val[0], t_im.val[0], s_re_im.val[0]); - INV_BOTJ_LANE(x_re.val[3], x_im.val[3], t_re.val[1], t_im.val[1], s_re_im.val[0]); - - INV_BOTJm_LANE(y_re.val[2], y_im.val[2], t_re.val[2], t_im.val[2], s_re_im.val[0]); - INV_BOTJm_LANE(y_re.val[3], y_im.val[3], t_re.val[3], t_im.val[3], s_re_im.val[0]); - - /* - * ( 0, 16) - ( 8, 24) - * ( 1, 17) - ( 9, 25) - * ( 0, 16) + ( 8, 24) - * ( 1, 17) + ( 9, 25) - * ( 8, 24) = @ * ( 0, 1) - * ( 9, 25) = @ * ( 0, 1) - * - * ( 2, 18) - ( 10, 26) - * ( 3, 19) - ( 11, 27) - * ( 2, 18) + ( 10, 26) - * ( 3, 19) + ( 11, 27) - * ( 10, 26) = @ * ( 0, 1) - * ( 11, 27) = @ * ( 0, 1) - * - * ( 4, 20) - ( 12, 28) - * ( 5, 21) - ( 13, 29) - * ( 4, 20) + ( 12, 28) - * ( 5, 21) + ( 13, 29) - * ( 12, 28) = @ * ( 0, 1) - * ( 13, 29) = @ * ( 0, 1) - * - * ( 6, 22) - ( 14, 30) - * ( 7, 23) - ( 15, 31) - * ( 6, 22) + ( 14, 30) - * ( 7, 23) + ( 15, 31) - * ( 14, 30) = @ * ( 0, 1) - * ( 15, 31) = @ * ( 0, 1) - */ - - - if ( (j >> 4) & 1) - { - INV_TOPJmx4(t_re, t_im, x_re, x_im, y_re, y_im); - } - else - { - INV_TOPJx4(t_re, t_im, x_re, x_im, y_re, y_im); - } - - vload(s_re_im.val[0], &fpr_tab2[k2]); - k2 += 2 * ((j & 31) == 16); - - if (last) - { - vfmuln(s_re_im.val[0], s_re_im.val[0], fpr_p2_tab[logn]); - vfmulnx4(x_re, x_re, fpr_p2_tab[logn]); - vfmulnx4(x_im, x_im, fpr_p2_tab[logn]); - } - vstorex4(&f[j], x_re); - vstorex4(&f[j + hn], x_im); - - if (logn == 5) - { - // Special case in fpr_tab_log2 where re == im - vfmulx4_i(t_re, t_re, s_re_im.val[0]); - vfmulx4_i(t_im, t_im, s_re_im.val[0]); - - vfaddx4(y_re, t_im, t_re); - vfsubx4(y_im, t_im, t_re); - } - else - { - if ((j >> 4) & 1) - { - INV_BOTJm_LANEx4(y_re, y_im, t_re, t_im, s_re_im.val[0]); - } - else - { - INV_BOTJ_LANEx4(y_re, y_im, t_re, t_im, s_re_im.val[0]); - } - } - - vstorex4(&f[j + 8], y_re); - vstorex4(&f[j + 8 + hn], y_im); - } -} - -static - void ZfN(iFFT_logn1)(fpr *f, const unsigned logn, const unsigned last) -{ - // Total SIMD register 26 = 24 + 2 - float64x2x4_t a_re, a_im, b_re, b_im, t_re, t_im; // 24 - float64x2_t s_re_im; // 2 - - const unsigned n = 1 << logn; - const unsigned hn = n >> 1; - const unsigned ht = n >> 2; - - for (unsigned j = 0; j < ht; j+= 8) - { - vloadx4(a_re, &f[j]); - vloadx4(a_im, &f[j + hn]); - vloadx4(b_re, &f[j + ht]); - vloadx4(b_im, &f[j + ht + hn]); - - INV_TOPJx4(t_re, t_im, a_re, a_im, b_re, b_im); - - s_re_im = vld1q_dup_f64(&fpr_tab_log2[0]); - - if (last) - { - vfmuln(s_re_im, s_re_im, fpr_p2_tab[logn]); - vfmulnx4(a_re, a_re, fpr_p2_tab[logn]); - vfmulnx4(a_im, a_im, fpr_p2_tab[logn]); - } - - vstorex4(&f[j], a_re); - vstorex4(&f[j + hn], a_im); - - vfmulx4_i(t_re, t_re, s_re_im); - vfmulx4_i(t_im, t_im, s_re_im); - - vfaddx4(b_re, t_im, t_re); - vfsubx4(b_im, t_im, t_re); - - vstorex4(&f[j + ht], b_re); - vstorex4(&f[j + ht + hn], b_im); - } -} - -// static -// void ZfN(iFFT_logn2)(fpr *f, const unsigned logn, const unsigned level, unsigned last) -// { -// const unsigned int falcon_n = 1 << logn; -// const unsigned int hn = falcon_n >> 1; - -// // Total SIMD register: 26 = 16 + 8 + 2 -// float64x2x4_t t_re, t_im; // 8 -// float64x2x2_t x1_re, x2_re, x1_im, x2_im, -// y1_re, y2_re, y1_im, y2_im; // 16 -// float64x2_t s1_re_im, s2_re_im; // 2 - -// const fpr *fpr_inv_tab1 = NULL, *fpr_inv_tab2 = NULL; -// unsigned l, len, start, j, k1, k2; -// unsigned bar = logn - 4 - 2; -// unsigned Jm; - -// for (l = 4; l < logn - level - 1; l += 2) -// { -// len = 1 << l; -// last -= 1; -// fpr_inv_tab1 = fpr_table[bar--]; -// fpr_inv_tab2 = fpr_table[bar--]; -// k1 = 0; k2 = 0; - -// for (start = 0; start < hn; start += 1 << (l + 2)) -// { -// vload(s1_re_im, &fpr_inv_tab1[k1]); -// vload(s2_re_im, &fpr_inv_tab2[k2]); -// k1 += 2; -// k2 += 2 * ((start & 127) == 64); -// if (!last) -// { -// vfmuln(s2_re_im, s2_re_im, fpr_p2_tab[logn]); -// } -// Jm = (start >> (l+ 2)) & 1; -// for (j = start; j < start + len; j += 4) -// { -// /* -// Level 6 -// * ( 0, 64) - ( 16, 80) -// * ( 1, 65) - ( 17, 81) -// * ( 0, 64) + ( 16, 80) -// * ( 1, 65) + ( 17, 81) -// * ( 16, 80) = @ * ( 0, 1) -// * ( 17, 81) = @ * ( 0, 1) -// * -// * ( 2, 66) - ( 18, 82) -// * ( 3, 67) - ( 19, 83) -// * ( 2, 66) + ( 18, 82) -// * ( 3, 67) + ( 19, 83) -// * ( 18, 82) = @ * ( 0, 1) -// * ( 19, 83) = @ * ( 0, 1) -// * -// * ( 32, 96) - ( 48, 112) -// * ( 33, 97) - ( 49, 113) -// * ( 32, 96) + ( 48, 112) -// * ( 33, 97) + ( 49, 113) -// * ( 48, 112) = j@ * ( 0, 1) -// * ( 49, 113) = j@ * ( 0, 1) -// * -// * ( 34, 98) - ( 50, 114) -// * ( 35, 99) - ( 51, 115) -// * ( 34, 98) + ( 50, 114) -// * ( 35, 99) + ( 51, 115) -// * ( 50, 114) = j@ * ( 0, 1) -// * ( 51, 115) = j@ * ( 0, 1) -// */ -// // x1: 0 -> 4 | 64 -> 67 -// // y1: 16 -> 19 | 80 -> 81 -// // x2: 32 -> 35 | 96 -> 99 -// // y2: 48 -> 51 | 112 -> 115 -// vloadx2(x1_re, &f[j]); -// vloadx2(x1_im, &f[j + hn]); -// vloadx2(y1_re, &f[j + len]); -// vloadx2(y1_im, &f[j + len + hn]); - -// INV_TOPJ (t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], y1_re.val[0], y1_im.val[0]); -// INV_TOPJ (t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], y1_re.val[1], y1_im.val[1]); - -// vloadx2(x2_re, &f[j + 2*len]); -// vloadx2(x2_im, &f[j + 2*len + hn]); -// vloadx2(y2_re, &f[j + 3*len]); -// vloadx2(y2_im, &f[j + 3*len + hn]); - -// INV_TOPJm(t_re.val[2], t_im.val[2], x2_re.val[0], x2_im.val[0], y2_re.val[0], y2_im.val[0]); -// INV_TOPJm(t_re.val[3], t_im.val[3], x2_re.val[1], x2_im.val[1], y2_re.val[1], y2_im.val[1]); - -// INV_BOTJ_LANE (y1_re.val[0], y1_im.val[0], t_re.val[0], t_im.val[0], s1_re_im); -// INV_BOTJ_LANE (y1_re.val[1], y1_im.val[1], t_re.val[1], t_im.val[1], s1_re_im); - -// INV_BOTJm_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s1_re_im); -// INV_BOTJm_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s1_re_im); -// /* -// * Level 7 -// * ( 0, 64) - ( 32, 96) -// * ( 1, 65) - ( 33, 97) -// * ( 0, 64) + ( 32, 96) -// * ( 1, 65) + ( 33, 97) -// * ( 32, 96) = @ * ( 0, 1) -// * ( 33, 97) = @ * ( 0, 1) -// * -// * ( 2, 66) - ( 34, 98) -// * ( 3, 67) - ( 35, 99) -// * ( 2, 66) + ( 34, 98) -// * ( 3, 67) + ( 35, 99) -// * ( 34, 98) = @ * ( 0, 1) -// * ( 35, 99) = @ * ( 0, 1) -// * ---- -// * ( 16, 80) - ( 48, 112) -// * ( 17, 81) - ( 49, 113) -// * ( 16, 80) + ( 48, 112) -// * ( 17, 81) + ( 49, 113) -// * ( 48, 112) = @ * ( 0, 1) -// * ( 49, 113) = @ * ( 0, 1) -// * -// * ( 18, 82) - ( 50, 114) -// * ( 19, 83) - ( 51, 115) -// * ( 18, 82) + ( 50, 114) -// * ( 19, 83) + ( 51, 115) -// * ( 50, 114) = @ * ( 0, 1) -// * ( 51, 115) = @ * ( 0, 1) -// */ - -// if (Jm) -// { -// INV_TOPJm(t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], x2_re.val[0], x2_im.val[0]); -// INV_TOPJm(t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], x2_re.val[1], x2_im.val[1]); - -// INV_TOPJm(t_re.val[2], t_im.val[2], y1_re.val[0], y1_im.val[0], y2_re.val[0], y2_im.val[0]); -// INV_TOPJm(t_re.val[3], t_im.val[3], y1_re.val[1], y1_im.val[1], y2_re.val[1], y2_im.val[1]); - -// INV_BOTJm_LANE(x2_re.val[0], x2_im.val[0], t_re.val[0], t_im.val[0], s2_re_im); -// INV_BOTJm_LANE(x2_re.val[1], x2_im.val[1], t_re.val[1], t_im.val[1], s2_re_im); -// INV_BOTJm_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s2_re_im); -// INV_BOTJm_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s2_re_im); -// } -// else -// { -// INV_TOPJ(t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], x2_re.val[0], x2_im.val[0]); -// INV_TOPJ(t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], x2_re.val[1], x2_im.val[1]); - -// INV_TOPJ(t_re.val[2], t_im.val[2], y1_re.val[0], y1_im.val[0], y2_re.val[0], y2_im.val[0]); -// INV_TOPJ(t_re.val[3], t_im.val[3], y1_re.val[1], y1_im.val[1], y2_re.val[1], y2_im.val[1]); - -// INV_BOTJ_LANE(x2_re.val[0], x2_im.val[0], t_re.val[0], t_im.val[0], s2_re_im); -// INV_BOTJ_LANE(x2_re.val[1], x2_im.val[1], t_re.val[1], t_im.val[1], s2_re_im); -// INV_BOTJ_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s2_re_im); -// INV_BOTJ_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s2_re_im); -// } - -// vstorex2(&f[j + 2*len], x2_re); -// vstorex2(&f[j + 2*len + hn], x2_im); - -// vstorex2(&f[j + 3*len], y2_re); -// vstorex2(&f[j + 3*len + hn], y2_im); - -// if (!last) -// { -// vfmuln(x1_re.val[0], x1_re.val[0], fpr_p2_tab[logn]); -// vfmuln(x1_re.val[1], x1_re.val[1], fpr_p2_tab[logn]); -// vfmuln(x1_im.val[0], x1_im.val[0], fpr_p2_tab[logn]); -// vfmuln(x1_im.val[1], x1_im.val[1], fpr_p2_tab[logn]); - -// vfmuln(y1_re.val[0], y1_re.val[0], fpr_p2_tab[logn]); -// vfmuln(y1_re.val[1], y1_re.val[1], fpr_p2_tab[logn]); -// vfmuln(y1_im.val[0], y1_im.val[0], fpr_p2_tab[logn]); -// vfmuln(y1_im.val[1], y1_im.val[1], fpr_p2_tab[logn]); -// } - -// vstorex2(&f[j], x1_re); -// vstorex2(&f[j + hn], x1_im); - -// vstorex2(&f[j + len], y1_re); -// vstorex2(&f[j + len + hn], y1_im); - -// } -// } -// } -// } - - - -static - void ZfN(iFFT_logn2)(fpr *f, const unsigned logn, const unsigned level, unsigned last) -{ - const unsigned int falcon_n = 1 << logn; - const unsigned int hn = falcon_n >> 1; - - // Total SIMD register: 26 = 16 + 8 + 2 - float64x2x4_t t_re, t_im; // 8 - float64x2x2_t x1_re, x2_re, x1_im, x2_im, - y1_re, y2_re, y1_im, y2_im; // 16 - float64x2_t s1_re_im, s2_re_im; // 2 - - const fpr *fpr_inv_tab1 = NULL, *fpr_inv_tab2 = NULL; - unsigned l, len, start, j, k1, k2; - unsigned bar = logn - 4; - - for (l = 4; l < logn - level - 1; l += 2) - { - len = 1 << l; - last -= 1; - fpr_inv_tab1 = fpr_table[bar--]; - fpr_inv_tab2 = fpr_table[bar--]; - k1 = 0; k2 = 0; - - for (start = 0; start < hn; start += 1 << (l + 2)) - { - vload(s1_re_im, &fpr_inv_tab1[k1]); - vload(s2_re_im, &fpr_inv_tab2[k2]); - k1 += 2; - k2 += 2 * ((start & 127) == 64); - if (!last) - { - vfmuln(s2_re_im, s2_re_im, fpr_p2_tab[logn]); - } - for (j = start; j < start + len; j += 4) - { - /* - Level 6 - * ( 0, 64) - ( 16, 80) - * ( 1, 65) - ( 17, 81) - * ( 0, 64) + ( 16, 80) - * ( 1, 65) + ( 17, 81) - * ( 16, 80) = @ * ( 0, 1) - * ( 17, 81) = @ * ( 0, 1) - * - * ( 2, 66) - ( 18, 82) - * ( 3, 67) - ( 19, 83) - * ( 2, 66) + ( 18, 82) - * ( 3, 67) + ( 19, 83) - * ( 18, 82) = @ * ( 0, 1) - * ( 19, 83) = @ * ( 0, 1) - * - * ( 32, 96) - ( 48, 112) - * ( 33, 97) - ( 49, 113) - * ( 32, 96) + ( 48, 112) - * ( 33, 97) + ( 49, 113) - * ( 48, 112) = j@ * ( 0, 1) - * ( 49, 113) = j@ * ( 0, 1) - * - * ( 34, 98) - ( 50, 114) - * ( 35, 99) - ( 51, 115) - * ( 34, 98) + ( 50, 114) - * ( 35, 99) + ( 51, 115) - * ( 50, 114) = j@ * ( 0, 1) - * ( 51, 115) = j@ * ( 0, 1) - */ - // x1: 0 -> 4 | 64 -> 67 - // y1: 16 -> 19 | 80 -> 81 - // x2: 32 -> 35 | 96 -> 99 - // y2: 48 -> 51 | 112 -> 115 - vloadx2(x1_re, &f[j]); - vloadx2(x1_im, &f[j + hn]); - vloadx2(y1_re, &f[j + len]); - vloadx2(y1_im, &f[j + len + hn]); - - INV_TOPJ (t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], y1_re.val[0], y1_im.val[0]); - INV_TOPJ (t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], y1_re.val[1], y1_im.val[1]); - - vloadx2(x2_re, &f[j + 2*len]); - vloadx2(x2_im, &f[j + 2*len + hn]); - vloadx2(y2_re, &f[j + 3*len]); - vloadx2(y2_im, &f[j + 3*len + hn]); - - INV_TOPJm(t_re.val[2], t_im.val[2], x2_re.val[0], x2_im.val[0], y2_re.val[0], y2_im.val[0]); - INV_TOPJm(t_re.val[3], t_im.val[3], x2_re.val[1], x2_im.val[1], y2_re.val[1], y2_im.val[1]); - - INV_BOTJ_LANE (y1_re.val[0], y1_im.val[0], t_re.val[0], t_im.val[0], s1_re_im); - INV_BOTJ_LANE (y1_re.val[1], y1_im.val[1], t_re.val[1], t_im.val[1], s1_re_im); - - INV_BOTJm_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s1_re_im); - INV_BOTJm_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s1_re_im); - /* - * Level 7 - * ( 0, 64) - ( 32, 96) - * ( 1, 65) - ( 33, 97) - * ( 0, 64) + ( 32, 96) - * ( 1, 65) + ( 33, 97) - * ( 32, 96) = @ * ( 0, 1) - * ( 33, 97) = @ * ( 0, 1) - * - * ( 2, 66) - ( 34, 98) - * ( 3, 67) - ( 35, 99) - * ( 2, 66) + ( 34, 98) - * ( 3, 67) + ( 35, 99) - * ( 34, 98) = @ * ( 0, 1) - * ( 35, 99) = @ * ( 0, 1) - * ---- - * ( 16, 80) - ( 48, 112) - * ( 17, 81) - ( 49, 113) - * ( 16, 80) + ( 48, 112) - * ( 17, 81) + ( 49, 113) - * ( 48, 112) = @ * ( 0, 1) - * ( 49, 113) = @ * ( 0, 1) - * - * ( 18, 82) - ( 50, 114) - * ( 19, 83) - ( 51, 115) - * ( 18, 82) + ( 50, 114) - * ( 19, 83) + ( 51, 115) - * ( 50, 114) = @ * ( 0, 1) - * ( 51, 115) = @ * ( 0, 1) - */ - - - INV_TOPJ(t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], x2_re.val[0], x2_im.val[0]); - INV_TOPJ(t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], x2_re.val[1], x2_im.val[1]); - - INV_TOPJ(t_re.val[2], t_im.val[2], y1_re.val[0], y1_im.val[0], y2_re.val[0], y2_im.val[0]); - INV_TOPJ(t_re.val[3], t_im.val[3], y1_re.val[1], y1_im.val[1], y2_re.val[1], y2_im.val[1]); - - INV_BOTJ_LANE(x2_re.val[0], x2_im.val[0], t_re.val[0], t_im.val[0], s2_re_im); - INV_BOTJ_LANE(x2_re.val[1], x2_im.val[1], t_re.val[1], t_im.val[1], s2_re_im); - INV_BOTJ_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s2_re_im); - INV_BOTJ_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s2_re_im); - - vstorex2(&f[j + 2*len], x2_re); - vstorex2(&f[j + 2*len + hn], x2_im); - - vstorex2(&f[j + 3*len], y2_re); - vstorex2(&f[j + 3*len + hn], y2_im); - - if (!last) - { - vfmuln(x1_re.val[0], x1_re.val[0], fpr_p2_tab[logn]); - vfmuln(x1_re.val[1], x1_re.val[1], fpr_p2_tab[logn]); - vfmuln(x1_im.val[0], x1_im.val[0], fpr_p2_tab[logn]); - vfmuln(x1_im.val[1], x1_im.val[1], fpr_p2_tab[logn]); - - vfmuln(y1_re.val[0], y1_re.val[0], fpr_p2_tab[logn]); - vfmuln(y1_re.val[1], y1_re.val[1], fpr_p2_tab[logn]); - vfmuln(y1_im.val[0], y1_im.val[0], fpr_p2_tab[logn]); - vfmuln(y1_im.val[1], y1_im.val[1], fpr_p2_tab[logn]); - } - - vstorex2(&f[j], x1_re); - vstorex2(&f[j + hn], x1_im); - - vstorex2(&f[j + len], y1_re); - vstorex2(&f[j + len + hn], y1_im); - - } - // - start += 1 << (l + 2); - if (start >= hn) break; - - vload(s1_re_im, &fpr_inv_tab1[k1]); - vload(s2_re_im, &fpr_inv_tab2[k2]); - k1 += 2; - k2 += 2 * ((start & 127) == 64); - if (!last) - { - vfmuln(s2_re_im, s2_re_im, fpr_p2_tab[logn]); - } - - for (j = start; j < start + len; j += 4) - { - /* - Level 6 - * ( 0, 64) - ( 16, 80) - * ( 1, 65) - ( 17, 81) - * ( 0, 64) + ( 16, 80) - * ( 1, 65) + ( 17, 81) - * ( 16, 80) = @ * ( 0, 1) - * ( 17, 81) = @ * ( 0, 1) - * - * ( 2, 66) - ( 18, 82) - * ( 3, 67) - ( 19, 83) - * ( 2, 66) + ( 18, 82) - * ( 3, 67) + ( 19, 83) - * ( 18, 82) = @ * ( 0, 1) - * ( 19, 83) = @ * ( 0, 1) - * - * ( 32, 96) - ( 48, 112) - * ( 33, 97) - ( 49, 113) - * ( 32, 96) + ( 48, 112) - * ( 33, 97) + ( 49, 113) - * ( 48, 112) = j@ * ( 0, 1) - * ( 49, 113) = j@ * ( 0, 1) - * - * ( 34, 98) - ( 50, 114) - * ( 35, 99) - ( 51, 115) - * ( 34, 98) + ( 50, 114) - * ( 35, 99) + ( 51, 115) - * ( 50, 114) = j@ * ( 0, 1) - * ( 51, 115) = j@ * ( 0, 1) - */ - // x1: 0 -> 4 | 64 -> 67 - // y1: 16 -> 19 | 80 -> 81 - // x2: 32 -> 35 | 96 -> 99 - // y2: 48 -> 51 | 112 -> 115 - vloadx2(x1_re, &f[j]); - vloadx2(x1_im, &f[j + hn]); - vloadx2(y1_re, &f[j + len]); - vloadx2(y1_im, &f[j + len + hn]); - - INV_TOPJ (t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], y1_re.val[0], y1_im.val[0]); - INV_TOPJ (t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], y1_re.val[1], y1_im.val[1]); - - vloadx2(x2_re, &f[j + 2*len]); - vloadx2(x2_im, &f[j + 2*len + hn]); - vloadx2(y2_re, &f[j + 3*len]); - vloadx2(y2_im, &f[j + 3*len + hn]); - - INV_TOPJm(t_re.val[2], t_im.val[2], x2_re.val[0], x2_im.val[0], y2_re.val[0], y2_im.val[0]); - INV_TOPJm(t_re.val[3], t_im.val[3], x2_re.val[1], x2_im.val[1], y2_re.val[1], y2_im.val[1]); - - INV_BOTJ_LANE (y1_re.val[0], y1_im.val[0], t_re.val[0], t_im.val[0], s1_re_im); - INV_BOTJ_LANE (y1_re.val[1], y1_im.val[1], t_re.val[1], t_im.val[1], s1_re_im); - - INV_BOTJm_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s1_re_im); - INV_BOTJm_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s1_re_im); - /* - * Level 7 - * ( 0, 64) - ( 32, 96) - * ( 1, 65) - ( 33, 97) - * ( 0, 64) + ( 32, 96) - * ( 1, 65) + ( 33, 97) - * ( 32, 96) = @ * ( 0, 1) - * ( 33, 97) = @ * ( 0, 1) - * - * ( 2, 66) - ( 34, 98) - * ( 3, 67) - ( 35, 99) - * ( 2, 66) + ( 34, 98) - * ( 3, 67) + ( 35, 99) - * ( 34, 98) = @ * ( 0, 1) - * ( 35, 99) = @ * ( 0, 1) - * ---- - * ( 16, 80) - ( 48, 112) - * ( 17, 81) - ( 49, 113) - * ( 16, 80) + ( 48, 112) - * ( 17, 81) + ( 49, 113) - * ( 48, 112) = @ * ( 0, 1) - * ( 49, 113) = @ * ( 0, 1) - * - * ( 18, 82) - ( 50, 114) - * ( 19, 83) - ( 51, 115) - * ( 18, 82) + ( 50, 114) - * ( 19, 83) + ( 51, 115) - * ( 50, 114) = @ * ( 0, 1) - * ( 51, 115) = @ * ( 0, 1) - */ - - INV_TOPJm(t_re.val[0], t_im.val[0], x1_re.val[0], x1_im.val[0], x2_re.val[0], x2_im.val[0]); - INV_TOPJm(t_re.val[1], t_im.val[1], x1_re.val[1], x1_im.val[1], x2_re.val[1], x2_im.val[1]); - - INV_TOPJm(t_re.val[2], t_im.val[2], y1_re.val[0], y1_im.val[0], y2_re.val[0], y2_im.val[0]); - INV_TOPJm(t_re.val[3], t_im.val[3], y1_re.val[1], y1_im.val[1], y2_re.val[1], y2_im.val[1]); - - INV_BOTJm_LANE(x2_re.val[0], x2_im.val[0], t_re.val[0], t_im.val[0], s2_re_im); - INV_BOTJm_LANE(x2_re.val[1], x2_im.val[1], t_re.val[1], t_im.val[1], s2_re_im); - INV_BOTJm_LANE(y2_re.val[0], y2_im.val[0], t_re.val[2], t_im.val[2], s2_re_im); - INV_BOTJm_LANE(y2_re.val[1], y2_im.val[1], t_re.val[3], t_im.val[3], s2_re_im); - - vstorex2(&f[j + 2*len], x2_re); - vstorex2(&f[j + 2*len + hn], x2_im); - - vstorex2(&f[j + 3*len], y2_re); - vstorex2(&f[j + 3*len + hn], y2_im); - - if (!last) - { - vfmuln(x1_re.val[0], x1_re.val[0], fpr_p2_tab[logn]); - vfmuln(x1_re.val[1], x1_re.val[1], fpr_p2_tab[logn]); - vfmuln(x1_im.val[0], x1_im.val[0], fpr_p2_tab[logn]); - vfmuln(x1_im.val[1], x1_im.val[1], fpr_p2_tab[logn]); - - vfmuln(y1_re.val[0], y1_re.val[0], fpr_p2_tab[logn]); - vfmuln(y1_re.val[1], y1_re.val[1], fpr_p2_tab[logn]); - vfmuln(y1_im.val[0], y1_im.val[0], fpr_p2_tab[logn]); - vfmuln(y1_im.val[1], y1_im.val[1], fpr_p2_tab[logn]); - } - - vstorex2(&f[j], x1_re); - vstorex2(&f[j + hn], x1_im); - - vstorex2(&f[j + len], y1_re); - vstorex2(&f[j + len + hn], y1_im); - - } - // - } - } -} - - -/* -* Support logn from [1, 10] -* Can be easily extended to logn > 10 -*/ -void ZfN(iFFT)(fpr *f, const unsigned logn) -{ - const unsigned level = (logn - 5) & 1; - - switch (logn) - { - case 2: - ZfN(iFFT_log2)(f); - break; - - case 3: - ZfN(iFFT_log3)(f); - break; - - case 4: - ZfN(iFFT_log4)(f); - break; - - case 5: - ZfN(iFFT_log5)(f, 5, 1); - break; - - case 6: - ZfN(iFFT_log5)(f, logn, 0); - ZfN(iFFT_logn1)(f, logn, 1); - break; - - case 7: - case 9: - ZfN(iFFT_log5)(f, logn, 0); - ZfN(iFFT_logn2)(f, logn, level, 1); - break; - - case 8: - case 10: - ZfN(iFFT_log5)(f, logn, 0); - ZfN(iFFT_logn2)(f, logn, level, 0); - ZfN(iFFT_logn1)(f, logn, 1); - break; - - default: - break; - } -} -#endif - -// generic fft stuff - -#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -void reim_ctwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim); -void reim_citwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim); - -void reim_fft16_ref(double* dre, double* dim, const void* pom); -void fill_reim_fft16_omegas(const double entry_pwr, double** omg); -void reim_fft8_ref(double* dre, double* dim, const void* pom); -void reim_twiddle_fft_ref(uint64_t h, double* re, double* im, double om[2]); -void fill_reim_twiddle_fft_ref(const double s, double** omg); -void reim_bitwiddle_fft_ref(uint64_t h, double* re, double* im, double om[4]); -void fill_reim_bitwiddle_fft_ref(const double s, double** omg); -void reim_fft_rec_16_ref(uint64_t m, double* re, double* im, double** omg); -void fill_reim_fft_rec_16_omegas(uint64_t m, double entry_pwr, double** omg); - -void fill_reim_twiddle_fft_omegas_ref(const double rs0, double** omg) { - (*omg)[0] = cos(2 * M_PI * rs0); - (*omg)[1] = sin(2 * M_PI * rs0); - *omg += 2; -} - -void fill_reim_bitwiddle_fft_omegas_ref(const double rs0, double** omg) { - double rs1 = 2. * rs0; - (*omg)[0] = cos(2 * M_PI * rs1); - (*omg)[1] = sin(2 * M_PI * rs1); - (*omg)[2] = cos(2 * M_PI * rs0); - (*omg)[3] = sin(2 * M_PI * rs0); - *omg += 4; -} - -#define reim_fft16_f reim_fft16_neon -#define reim_fft16_pom_offset 16 -#define fill_reim_fft16_omegas_f fill_reim_fft16_omegas_neon -// currently, m=4 uses the ref implem (corner case with low impact) TODO!! -#define reim_fft8_f reim_fft8_ref -#define reim_fft8_pom_offset 8 -#define fill_reim_fft8_omegas_f fill_reim_fft8_omegas -// currently, m=4 uses the ref implem (corner case with low impact) TODO!! -#define reim_fft4_f reim_fft4_ref -#define reim_fft4_pom_offset 4 -#define fill_reim_fft4_omegas_f fill_reim_fft4_omegas -// m = 2 will use the ref implem, since intrinsics don't provide any speed-up -#define reim_fft2_f reim_fft2_ref -#define reim_fft2_pom_offset 2 -#define fill_reim_fft2_omegas_f fill_reim_fft2_omegas - -// neon twiddle use the same omegas layout as the ref implem -#define reim_twiddle_fft_f reim_twiddle_fft_neon -#define reim_twiddle_fft_pom_offset 2 -#define fill_reim_twiddle_fft_omegas_f fill_reim_twiddle_fft_omegas_ref - -// neon bi-twiddle use the same omegas layout as the ref implem -#define reim_bitwiddle_fft_f reim_bitwiddle_fft_neon -#define reim_bitwiddle_fft_pom_offset 4 -#define fill_reim_bitwiddle_fft_omegas_f fill_reim_bitwiddle_fft_omegas_ref - -// template functions to produce -#define reim_fft_bfs_16_f reim_fft_bfs_16_neon -#define fill_reim_fft_bfs_16_omegas_f fill_reim_fft_bfs_16_omegas_neon -#define reim_fft_rec_16_f reim_fft_rec_16_neon -#define fill_reim_fft_rec_16_omegas_f fill_reim_fft_rec_16_omegas_neon -#define reim_fft_f reim_fft_neon -#define fill_reim_fft_omegas_f fill_reim_fft_omegas_neon - -#include "reim_fft_core_template.h" - -EXPORT REIM_FFT_PRECOMP* new_reim_fft_precomp_neon(uint32_t m, uint32_t num_buffers) { - const uint64_t OMG_SPACE = ceilto64b(2 * m * sizeof(double)); - const uint64_t BUF_SIZE = ceilto64b(2 * m * sizeof(double)); - void* reps = malloc(sizeof(REIM_FFT_PRECOMP) // base - + 63 // padding - + OMG_SPACE // tables //TODO 16? - + num_buffers * BUF_SIZE // buffers - ); - uint64_t aligned_addr = ceilto64b((uint64_t)(reps) + sizeof(REIM_FFT_PRECOMP)); - REIM_FFT_PRECOMP* r = (REIM_FFT_PRECOMP*)reps; - r->m = m; - r->buf_size = BUF_SIZE; - r->powomegas = (double*)aligned_addr; - r->aligned_buffers = (void*)(aligned_addr + OMG_SPACE); - // fill in powomegas - double* omg = (double*)r->powomegas; - fill_reim_fft_omegas_f(m, 0.25, &omg); - if (((uint64_t)omg) - aligned_addr > OMG_SPACE) abort(); - // dispatch the right implementation - //{ - // if (CPU_SUPPORTS("fma")) { - // r->function = reim_fft_avx2_fma; - // } else { - // r->function = reim_fft_ref; - // } - //} - r->function = reim_fft_f; - return reps; -}
@@ -1,128 +0,0 @@
-#ifndef SPQLIOS_REIM_FFT_PRIVATE_H -#define SPQLIOS_REIM_FFT_PRIVATE_H - -#include "../commons_private.h" -#include "reim_fft.h" - -#define STATIC_ASSERT(condition) (void)sizeof(char[-1 + 2 * !!(condition)]) - -typedef struct reim_twiddle_precomp REIM_FFTVEC_TWIDDLE_PRECOMP; -typedef struct reim_bitwiddle_precomp REIM_FFTVEC_BITWIDDLE_PRECOMP; - -typedef void (*FFT_FUNC)(const REIM_FFT_PRECOMP*, double*); -typedef void (*IFFT_FUNC)(const REIM_IFFT_PRECOMP*, double*); -typedef void (*FFTVEC_ADD_FUNC)(const REIM_FFTVEC_ADD_PRECOMP*, double*, const double*, const double*); -typedef void (*FFTVEC_SUB_FUNC)(const REIM_FFTVEC_SUB_PRECOMP*, double*, const double*, const double*); -typedef void (*FFTVEC_MUL_FUNC)(const REIM_FFTVEC_MUL_PRECOMP*, double*, const double*, const double*); -typedef void (*FFTVEC_ADDMUL_FUNC)(const REIM_FFTVEC_ADDMUL_PRECOMP*, double*, const double*, const double*); -typedef void (*FROM_ZNX32_FUNC)(const REIM_FROM_ZNX32_PRECOMP*, void*, const int32_t*); -typedef void (*FROM_ZNX64_FUNC)(const REIM_FROM_ZNX64_PRECOMP*, void*, const int64_t*); -typedef void (*FROM_TNX32_FUNC)(const REIM_FROM_TNX32_PRECOMP*, void*, const int32_t*); -typedef void (*TO_TNX32_FUNC)(const REIM_TO_TNX32_PRECOMP*, int32_t*, const void*); -typedef void (*TO_TNX_FUNC)(const REIM_TO_TNX_PRECOMP*, double*, const double*); -typedef void (*TO_ZNX64_FUNC)(const REIM_TO_ZNX64_PRECOMP*, int64_t*, const void*); -typedef void (*FFTVEC_TWIDDLE_FUNC)(const REIM_FFTVEC_TWIDDLE_PRECOMP*, void*, const void*, const void*); -typedef void (*FFTVEC_BITWIDDLE_FUNC)(const REIM_FFTVEC_BITWIDDLE_PRECOMP*, void*, uint64_t, const void*); - -typedef void (*FFTVEC_AUTOMORPHISM_APPLY)(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* precomp, int64_t p, double* r, - const double* a, uint64_t a_size); -typedef uint64_t (*FFTVEC_AUTOMORPHISM_APPLY_INPLACE_TMP_BYTES)(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* precomp); -typedef void (*FFTVEC_AUTOMORPHISM_APPLY_INPLACE)(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* precomp, int64_t p, double* a, - uint64_t a_size, uint8_t* tmp_bytes); -typedef struct reim_fftvec_automorphism_funcs { - FFTVEC_AUTOMORPHISM_APPLY apply; - FFTVEC_AUTOMORPHISM_APPLY_INPLACE apply_inplace; - FFTVEC_AUTOMORPHISM_APPLY_INPLACE_TMP_BYTES apply_inplace_tmp_bytes; -} FFTVEC_AUTOMORPHISM_FUNCS; - -typedef struct reim_fft_precomp { - FFT_FUNC function; - int64_t m; ///< complex dimension warning: reim uses n=2N=4m - uint64_t buf_size; ///< size of aligned_buffers (multiple of 64B) - double* powomegas; ///< 64B aligned - void* aligned_buffers; ///< 64B aligned -} REIM_FFT_PRECOMP; - -typedef struct reim_ifft_precomp { - IFFT_FUNC function; - int64_t m; // warning: reim uses n=2N=4m - uint64_t buf_size; ///< size of aligned_buffers (multiple of 64B) - double* powomegas; - void* aligned_buffers; -} REIM_IFFT_PRECOMP; - -typedef struct reim_add_precomp { - FFTVEC_ADD_FUNC function; - int64_t m; -} REIM_FFTVEC_ADD_PRECOMP; - -typedef struct reim_sub_precomp { - FFTVEC_SUB_FUNC function; - int64_t m; -} REIM_FFTVEC_SUB_PRECOMP; - -typedef struct reim_mul_precomp { - FFTVEC_MUL_FUNC function; - int64_t m; -} REIM_FFTVEC_MUL_PRECOMP; - -typedef struct reim_addmul_precomp { - FFTVEC_ADDMUL_FUNC function; - int64_t m; -} REIM_FFTVEC_ADDMUL_PRECOMP; - -typedef struct reim_fftvec_automorphism_precomp { - FFTVEC_AUTOMORPHISM_FUNCS function; - int64_t m; - uint64_t* irev; -} REIM_FFTVEC_AUTOMORPHISM_PRECOMP; - -struct reim_from_znx32_precomp { - FROM_ZNX32_FUNC function; - int64_t m; -}; - -struct reim_from_znx64_precomp { - FROM_ZNX64_FUNC function; - int64_t m; -}; - -struct reim_from_tnx32_precomp { - FROM_TNX32_FUNC function; - int64_t m; -}; - -struct reim_to_tnx32_precomp { - TO_TNX32_FUNC function; - int64_t m; - double divisor; -}; - -struct reim_to_tnx_precomp { - TO_TNX_FUNC function; - int64_t m; - double divisor; - uint32_t log2overhead; - double add_cst; - uint64_t mask_and; - uint64_t mask_or; - double sub_cst; -}; - -struct reim_to_znx64_precomp { - TO_ZNX64_FUNC function; - int64_t m; - double divisor; -}; - -struct reim_twiddle_precomp { - FFTVEC_TWIDDLE_FUNC function; - int64_t m; -}; - -struct reim_bitwiddle_precomp { - FFTVEC_BITWIDDLE_FUNC function; - int64_t m; -}; - -#endif // SPQLIOS_REIM_FFT_PRIVATE_H
@@ -1,450 +0,0 @@
-#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -EXPORT void reim_fft_simple(uint32_t m, void* data) { - static REIM_FFT_PRECOMP* p[31] = {0}; - REIM_FFT_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_fft_precomp(m, 0); - (*f)->function(*f, data); -} - -EXPORT void reim_ifft_simple(uint32_t m, void* data) { - static REIM_IFFT_PRECOMP* p[31] = {0}; - REIM_IFFT_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_ifft_precomp(m, 0); - (*f)->function(*f, data); -} - -EXPORT void reim_fftvec_add_simple(uint32_t m, void* r, const void* a, const void* b) { - static REIM_FFTVEC_ADD_PRECOMP* p[31] = {0}; - REIM_FFTVEC_ADD_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_fftvec_add_precomp(m); - (*f)->function(*f, r, a, b); -} - -EXPORT void reim_fftvec_sub_simple(uint32_t m, void* r, const void* a, const void* b) { - static REIM_FFTVEC_SUB_PRECOMP* p[31] = {0}; - REIM_FFTVEC_SUB_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_fftvec_sub_precomp(m); - (*f)->function(*f, r, a, b); -} - -EXPORT void reim_fftvec_mul_simple(uint32_t m, void* r, const void* a, const void* b) { - static REIM_FFTVEC_MUL_PRECOMP* p[31] = {0}; - REIM_FFTVEC_MUL_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_fftvec_mul_precomp(m); - (*f)->function(*f, r, a, b); -} - -EXPORT void reim_fftvec_addmul_simple(uint32_t m, void* r, const void* a, const void* b) { - static REIM_FFTVEC_ADDMUL_PRECOMP* p[31] = {0}; - REIM_FFTVEC_ADDMUL_PRECOMP** f = p + log2m(m); - if (!*f) *f = new_reim_fftvec_addmul_precomp(m); - (*f)->function(*f, r, a, b); -} - -void reim_ctwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim) { - double newrt = *rb * omre - *ib * omim; - double newit = *rb * omim + *ib * omre; - *rb = *ra - newrt; - *ib = *ia - newit; - *ra = *ra + newrt; - *ia = *ia + newit; -} - -// i (omre + i omim) = -omim + i omre -void reim_citwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim) { - double newrt = -*rb * omim - *ib * omre; - double newit = *rb * omre - *ib * omim; - *rb = *ra - newrt; - *ib = *ia - newit; - *ra = *ra + newrt; - *ia = *ia + newit; -} - -void reim_fft16_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omre = om[0]; - double omim = om[1]; - reim_ctwiddle(&dre[0], &dim[0], &dre[8], &dim[8], omre, omim); - reim_ctwiddle(&dre[1], &dim[1], &dre[9], &dim[9], omre, omim); - reim_ctwiddle(&dre[2], &dim[2], &dre[10], &dim[10], omre, omim); - reim_ctwiddle(&dre[3], &dim[3], &dre[11], &dim[11], omre, omim); - reim_ctwiddle(&dre[4], &dim[4], &dre[12], &dim[12], omre, omim); - reim_ctwiddle(&dre[5], &dim[5], &dre[13], &dim[13], omre, omim); - reim_ctwiddle(&dre[6], &dim[6], &dre[14], &dim[14], omre, omim); - reim_ctwiddle(&dre[7], &dim[7], &dre[15], &dim[15], omre, omim); - } - { - double omre = om[2]; - double omim = om[3]; - reim_ctwiddle(&dre[0], &dim[0], &dre[4], &dim[4], omre, omim); - reim_ctwiddle(&dre[1], &dim[1], &dre[5], &dim[5], omre, omim); - reim_ctwiddle(&dre[2], &dim[2], &dre[6], &dim[6], omre, omim); - reim_ctwiddle(&dre[3], &dim[3], &dre[7], &dim[7], omre, omim); - reim_citwiddle(&dre[8], &dim[8], &dre[12], &dim[12], omre, omim); - reim_citwiddle(&dre[9], &dim[9], &dre[13], &dim[13], omre, omim); - reim_citwiddle(&dre[10], &dim[10], &dre[14], &dim[14], omre, omim); - reim_citwiddle(&dre[11], &dim[11], &dre[15], &dim[15], omre, omim); - } - { - double omare = om[4]; - double omaim = om[5]; - double ombre = om[6]; - double ombim = om[7]; - reim_ctwiddle(&dre[0], &dim[0], &dre[2], &dim[2], omare, omaim); - reim_ctwiddle(&dre[1], &dim[1], &dre[3], &dim[3], omare, omaim); - reim_citwiddle(&dre[4], &dim[4], &dre[6], &dim[6], omare, omaim); - reim_citwiddle(&dre[5], &dim[5], &dre[7], &dim[7], omare, omaim); - reim_ctwiddle(&dre[8], &dim[8], &dre[10], &dim[10], ombre, ombim); - reim_ctwiddle(&dre[9], &dim[9], &dre[11], &dim[11], ombre, ombim); - reim_citwiddle(&dre[12], &dim[12], &dre[14], &dim[14], ombre, ombim); - reim_citwiddle(&dre[13], &dim[13], &dre[15], &dim[15], ombre, ombim); - } - { - double omare = om[8]; - double ombre = om[9]; - double omcre = om[10]; - double omdre = om[11]; - double omaim = om[12]; - double ombim = om[13]; - double omcim = om[14]; - double omdim = om[15]; - reim_ctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - reim_citwiddle(&dre[2], &dim[2], &dre[3], &dim[3], omare, omaim); - reim_ctwiddle(&dre[4], &dim[4], &dre[5], &dim[5], ombre, ombim); - reim_citwiddle(&dre[6], &dim[6], &dre[7], &dim[7], ombre, ombim); - reim_ctwiddle(&dre[8], &dim[8], &dre[9], &dim[9], omcre, omcim); - reim_citwiddle(&dre[10], &dim[10], &dre[11], &dim[11], omcre, omcim); - reim_ctwiddle(&dre[12], &dim[12], &dre[13], &dim[13], omdre, omdim); - reim_citwiddle(&dre[14], &dim[14], &dre[15], &dim[15], omdre, omdim); - } -} - -void fill_reim_fft16_omegas(const double entry_pwr, double** omg) { - const double j_pow = 1. / 8.; - const double k_pow = 1. / 16.; - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - const double pin_4 = entry_pwr / 8.; - const double pin_8 = entry_pwr / 16.; - // 0 and 1 are real and imag of om - (*omg)[0] = cos(2. * M_PI * pin); - (*omg)[1] = sin(2. * M_PI * pin); - // 2 and 3 are real and imag of om^1/2 - (*omg)[2] = cos(2. * M_PI * (pin_2)); - (*omg)[3] = sin(2. * M_PI * (pin_2)); - // (4,5) and (6,7) are real and imag of om^1/4 and j.om^1/4 - (*omg)[4] = cos(2. * M_PI * (pin_4)); - (*omg)[5] = sin(2. * M_PI * (pin_4)); - (*omg)[6] = cos(2. * M_PI * (pin_4 + j_pow)); - (*omg)[7] = sin(2. * M_PI * (pin_4 + j_pow)); - // ((8,9,10,11),(12,13,14,15)) are 4 reals then 4 imag of om^1/8*(1,k,j,kj) - (*omg)[8] = cos(2. * M_PI * (pin_8)); - (*omg)[9] = cos(2. * M_PI * (pin_8 + j_pow)); - (*omg)[10] = cos(2. * M_PI * (pin_8 + k_pow)); - (*omg)[11] = cos(2. * M_PI * (pin_8 + j_pow + k_pow)); - (*omg)[12] = sin(2. * M_PI * (pin_8)); - (*omg)[13] = sin(2. * M_PI * (pin_8 + j_pow)); - (*omg)[14] = sin(2. * M_PI * (pin_8 + k_pow)); - (*omg)[15] = sin(2. * M_PI * (pin_8 + j_pow + k_pow)); - *omg += 16; -} - -void reim_fft8_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omre = om[0]; - double omim = om[1]; - reim_ctwiddle(&dre[0], &dim[0], &dre[4], &dim[4], omre, omim); - reim_ctwiddle(&dre[1], &dim[1], &dre[5], &dim[5], omre, omim); - reim_ctwiddle(&dre[2], &dim[2], &dre[6], &dim[6], omre, omim); - reim_ctwiddle(&dre[3], &dim[3], &dre[7], &dim[7], omre, omim); - } - { - double omare = om[2]; - double omaim = om[3]; - reim_ctwiddle(&dre[0], &dim[0], &dre[2], &dim[2], omare, omaim); - reim_ctwiddle(&dre[1], &dim[1], &dre[3], &dim[3], omare, omaim); - reim_citwiddle(&dre[4], &dim[4], &dre[6], &dim[6], omare, omaim); - reim_citwiddle(&dre[5], &dim[5], &dre[7], &dim[7], omare, omaim); - } - { - double omare = om[4]; - double ombre = om[5]; - double omaim = om[6]; - double ombim = om[7]; - reim_ctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - reim_citwiddle(&dre[2], &dim[2], &dre[3], &dim[3], omare, omaim); - reim_ctwiddle(&dre[4], &dim[4], &dre[5], &dim[5], ombre, ombim); - reim_citwiddle(&dre[6], &dim[6], &dre[7], &dim[7], ombre, ombim); - } -} - -void fill_reim_fft8_omegas(const double entry_pwr, double** omg) { - const double j_pow = 1. / 8.; - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - const double pin_4 = entry_pwr / 8.; - // 0 and 1 are real and imag of om - (*omg)[0] = cos(2. * M_PI * pin); - (*omg)[1] = sin(2. * M_PI * pin); - // 2 and 3 are real and imag of om^1/2 - (*omg)[2] = cos(2. * M_PI * (pin_2)); - (*omg)[3] = sin(2. * M_PI * (pin_2)); - // (4,5) and (6,7) are real and imag of om^1/4 and j.om^1/4 - (*omg)[4] = cos(2. * M_PI * (pin_4)); - (*omg)[5] = cos(2. * M_PI * (pin_4 + j_pow)); - (*omg)[6] = sin(2. * M_PI * (pin_4)); - (*omg)[7] = sin(2. * M_PI * (pin_4 + j_pow)); - *omg += 8; -} - -void reim_fft4_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omare = om[0]; - double omaim = om[1]; - reim_ctwiddle(&dre[0], &dim[0], &dre[2], &dim[2], omare, omaim); - reim_ctwiddle(&dre[1], &dim[1], &dre[3], &dim[3], omare, omaim); - } - { - double omare = om[2]; - double omaim = om[3]; - reim_ctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - reim_citwiddle(&dre[2], &dim[2], &dre[3], &dim[3], omare, omaim); - } -} - -void fill_reim_fft4_omegas(const double entry_pwr, double** omg) { - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - // 0 and 1 are real and imag of om - (*omg)[0] = cos(2. * M_PI * pin); - (*omg)[1] = sin(2. * M_PI * pin); - // 2 and 3 are real and imag of om^1/2 - (*omg)[2] = cos(2. * M_PI * (pin_2)); - (*omg)[3] = sin(2. * M_PI * (pin_2)); - *omg += 4; -} - -void reim_fft2_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omare = om[0]; - double omaim = om[1]; - reim_ctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - } -} - -void fill_reim_fft2_omegas(const double entry_pwr, double** omg) { - const double pin = entry_pwr / 2.; - // 0 and 1 are real and imag of om - (*omg)[0] = cos(2. * M_PI * pin); - (*omg)[1] = sin(2. * M_PI * pin); - *omg += 2; -} - -void reim_twiddle_fft_ref(uint64_t h, double* re, double* im, double om[2]) { - for (uint64_t i = 0; i < h; ++i) { - reim_ctwiddle(&re[i], &im[i], &re[h + i], &im[h + i], om[0], om[1]); - } -} - -void reim_bitwiddle_fft_ref(uint64_t h, double* re, double* im, double om[4]) { - double* r0 = re; - double* r1 = re + h; - double* r2 = re + 2 * h; - double* r3 = re + 3 * h; - double* i0 = im; - double* i1 = im + h; - double* i2 = im + 2 * h; - double* i3 = im + 3 * h; - for (uint64_t i = 0; i < h; ++i) { - reim_ctwiddle(&r0[i], &i0[i], &r2[i], &i2[i], om[0], om[1]); - reim_ctwiddle(&r1[i], &i1[i], &r3[i], &i3[i], om[0], om[1]); - } - for (uint64_t i = 0; i < h; ++i) { - reim_ctwiddle(&r0[i], &i0[i], &r1[i], &i1[i], om[2], om[3]); - reim_citwiddle(&r2[i], &i2[i], &r3[i], &i3[i], om[2], om[3]); - } -} - -void reim_fft_bfs_16_ref(uint64_t m, double* re, double* im, double** omg) { - uint64_t log2m = log2(m); - uint64_t mm = m; - if (log2m % 2 != 0) { - uint64_t h = mm >> 1; - // do the first twiddle iteration normally - reim_twiddle_fft_ref(h, re, im, *omg); - *omg += 2; - mm = h; - } - while (mm > 16) { - uint64_t h = mm >> 2; - for (uint64_t off = 0; off < m; off += mm) { - reim_bitwiddle_fft_ref(h, re + off, im + off, *omg); - *omg += 4; - } - mm = h; - } - if (mm != 16) abort(); // bug! - for (uint64_t off = 0; off < m; off += 16) { - reim_fft16_ref(re + off, im + off, *omg); - *omg += 16; - } -} - -void fill_reim_fft_bfs_16_omegas(uint64_t m, double entry_pwr, double** omg) { - uint64_t log2m = log2(m); - uint64_t mm = m; - double ss = entry_pwr; - if (log2m % 2 != 0) { - uint64_t h = mm >> 1; - double s = ss / 2.; - // do the first twiddle iteration normally - (*omg)[0] = cos(2 * M_PI * s); - (*omg)[1] = sin(2 * M_PI * s); - *omg += 2; - mm = h; - ss = s; - } - while (mm > 16) { - uint64_t h = mm >> 2; - double s = ss / 4.; - for (uint64_t off = 0; off < m; off += mm) { - double rs0 = s + fracrevbits(off / mm) / 4.; - double rs1 = 2. * rs0; - (*omg)[0] = cos(2 * M_PI * rs1); - (*omg)[1] = sin(2 * M_PI * rs1); - (*omg)[2] = cos(2 * M_PI * rs0); - (*omg)[3] = sin(2 * M_PI * rs0); - *omg += 4; - } - mm = h; - ss = s; - } - if (mm != 16) abort(); // bug! - for (uint64_t off = 0; off < m; off += 16) { - double s = ss + fracrevbits(off / 16); - fill_reim_fft16_omegas(s, omg); - } -} - -void reim_fft_rec_16_ref(uint64_t m, double* re, double* im, double** omg) { - if (m <= 2048) return reim_fft_bfs_16_ref(m, re, im, omg); - const uint32_t h = m / 2; - reim_twiddle_fft_ref(h, re, im, *omg); - *omg += 2; - reim_fft_rec_16_ref(h, re, im, omg); - reim_fft_rec_16_ref(h, re + h, im + h, omg); -} - -void fill_reim_fft_rec_16_omegas(uint64_t m, double entry_pwr, double** omg) { - if (m <= 2048) return fill_reim_fft_bfs_16_omegas(m, entry_pwr, omg); - const uint64_t h = m / 2; - const double s = entry_pwr / 2; - (*omg)[0] = cos(2 * M_PI * s); - (*omg)[1] = sin(2 * M_PI * s); - *omg += 2; - fill_reim_fft_rec_16_omegas(h, s, omg); - fill_reim_fft_rec_16_omegas(h, s + 0.5, omg); -} - -void reim_fft_ref(const REIM_FFT_PRECOMP* precomp, double* dat) { - const int32_t m = precomp->m; - double* omg = precomp->powomegas; - double* re = dat; - double* im = dat + m; - if (m <= 16) { - switch (m) { - case 1: - return; - case 2: - return reim_fft2_ref(re, im, omg); - case 4: - return reim_fft4_ref(re, im, omg); - case 8: - return reim_fft8_ref(re, im, omg); - case 16: - return reim_fft16_ref(re, im, omg); - default: - abort(); // m is not a power of 2 - } - } - if (m <= 2048) return reim_fft_bfs_16_ref(m, re, im, &omg); - return reim_fft_rec_16_ref(m, re, im, &omg); -} - -EXPORT REIM_FFT_PRECOMP* new_reim_fft_precomp(uint32_t m, uint32_t num_buffers) { - const uint64_t OMG_SPACE = ceilto64b(2 * m * sizeof(double)); - const uint64_t BUF_SIZE = ceilto64b(2 * m * sizeof(double)); - void* reps = malloc(sizeof(REIM_FFT_PRECOMP) // base - + 63 // padding - + OMG_SPACE // tables //TODO 16? - + num_buffers * BUF_SIZE // buffers - ); - uint64_t aligned_addr = ceilto64b((uint64_t)(reps) + sizeof(REIM_FFT_PRECOMP)); - REIM_FFT_PRECOMP* r = (REIM_FFT_PRECOMP*)reps; - r->m = m; - r->buf_size = BUF_SIZE; - r->powomegas = (double*)aligned_addr; - r->aligned_buffers = (void*)(aligned_addr + OMG_SPACE); - // fill in powomegas - double* omg = (double*)r->powomegas; - if (m <= 16) { - switch (m) { - case 1: - break; - case 2: - fill_reim_fft2_omegas(0.25, &omg); - break; - case 4: - fill_reim_fft4_omegas(0.25, &omg); - break; - case 8: - fill_reim_fft8_omegas(0.25, &omg); - break; - case 16: - fill_reim_fft16_omegas(0.25, &omg); - break; - default: - abort(); // m is not a power of 2 - } - } else if (m <= 2048) { - fill_reim_fft_bfs_16_omegas(m, 0.25, &omg); - } else { - fill_reim_fft_rec_16_omegas(m, 0.25, &omg); - } - if (((uint64_t)omg) - aligned_addr > OMG_SPACE) abort(); - // dispatch the right implementation - { - if (CPU_SUPPORTS("fma")) { - r->function = reim_fft_avx2_fma; - } else { - r->function = reim_fft_ref; - } - } - return reps; -} - -void reim_naive_fft(uint64_t m, double entry_pwr, double* re, double* im) { - if (m == 1) return; - // twiddle - const uint64_t h = m / 2; - const double s = entry_pwr / 2.; - const double sre = cos(2 * M_PI * s); - const double sim = sin(2 * M_PI * s); - for (uint64_t j = 0; j < h; ++j) { - double pre = re[h + j] * sre - im[h + j] * sim; - double pim = im[h + j] * sre + re[h + j] * sim; - re[h + j] = re[j] - pre; - im[h + j] = im[j] - pim; - re[j] += pre; - im[j] += pim; - } - reim_naive_fft(h, s, re, im); - reim_naive_fft(h, s + 0.5, re + h, im + h); -}
@@ -1,137 +0,0 @@
-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim_fft_private.h" - -EXPORT void reim_fftvec_addmul_fma(const REIM_FFTVEC_ADDMUL_PRECOMP* precomp, double* r, const double* a, - const double* b) { - const uint64_t m = precomp->m; - double* rr_ptr = r; - double* ri_ptr = r + m; - const double* ar_ptr = a; - const double* ai_ptr = a + m; - const double* br_ptr = b; - const double* bi_ptr = b + m; - - const double* const rend_ptr = ri_ptr; - while (rr_ptr != rend_ptr) { - __m256d rr = _mm256_loadu_pd(rr_ptr); - __m256d ri = _mm256_loadu_pd(ri_ptr); - const __m256d ar = _mm256_loadu_pd(ar_ptr); - const __m256d ai = _mm256_loadu_pd(ai_ptr); - const __m256d br = _mm256_loadu_pd(br_ptr); - const __m256d bi = _mm256_loadu_pd(bi_ptr); - - rr = _mm256_fmsub_pd(ai, bi, rr); - rr = _mm256_fmsub_pd(ar, br, rr); - ri = _mm256_fmadd_pd(ar, bi, ri); - ri = _mm256_fmadd_pd(ai, br, ri); - - _mm256_storeu_pd(rr_ptr, rr); - _mm256_storeu_pd(ri_ptr, ri); - - rr_ptr += 4; - ri_ptr += 4; - ar_ptr += 4; - ai_ptr += 4; - br_ptr += 4; - bi_ptr += 4; - } -} - -EXPORT void reim_fftvec_mul_fma(const REIM_FFTVEC_MUL_PRECOMP* precomp, double* r, const double* a, const double* b) { - const uint64_t m = precomp->m; - double* rr_ptr = r; - double* ri_ptr = r + m; - const double* ar_ptr = a; - const double* ai_ptr = a + m; - const double* br_ptr = b; - const double* bi_ptr = b + m; - - const double* const rend_ptr = ri_ptr; - while (rr_ptr != rend_ptr) { - const __m256d ar = _mm256_loadu_pd(ar_ptr); - const __m256d ai = _mm256_loadu_pd(ai_ptr); - const __m256d br = _mm256_loadu_pd(br_ptr); - const __m256d bi = _mm256_loadu_pd(bi_ptr); - - const __m256d t1 = _mm256_mul_pd(ai, bi); - const __m256d t2 = _mm256_mul_pd(ar, bi); - - __m256d rr = _mm256_fmsub_pd(ar, br, t1); - __m256d ri = _mm256_fmadd_pd(ai, br, t2); - - _mm256_storeu_pd(rr_ptr, rr); - _mm256_storeu_pd(ri_ptr, ri); - - rr_ptr += 4; - ri_ptr += 4; - ar_ptr += 4; - ai_ptr += 4; - br_ptr += 4; - bi_ptr += 4; - } -} - -EXPORT void reim_fftvec_add_fma(const REIM_FFTVEC_ADD_PRECOMP* precomp, double* r, const double* a, const double* b) { - const uint64_t m = precomp->m; - double* rr_ptr = r; - double* ri_ptr = r + m; - const double* ar_ptr = a; - const double* ai_ptr = a + m; - const double* br_ptr = b; - const double* bi_ptr = b + m; - - const double* const rend_ptr = ri_ptr; - while (rr_ptr != rend_ptr) { - const __m256d ar = _mm256_loadu_pd(ar_ptr); - const __m256d ai = _mm256_loadu_pd(ai_ptr); - const __m256d br = _mm256_loadu_pd(br_ptr); - const __m256d bi = _mm256_loadu_pd(bi_ptr); - - __m256d rr = _mm256_add_pd(ar, br); - __m256d ri = _mm256_add_pd(ai, bi); - - _mm256_storeu_pd(rr_ptr, rr); - _mm256_storeu_pd(ri_ptr, ri); - - rr_ptr += 4; - ri_ptr += 4; - ar_ptr += 4; - ai_ptr += 4; - br_ptr += 4; - bi_ptr += 4; - } -} - -EXPORT void reim_fftvec_sub_fma(const REIM_FFTVEC_ADD_PRECOMP* precomp, double* r, const double* a, const double* b) { - const uint64_t m = precomp->m; - double* rr_ptr = r; - double* ri_ptr = r + m; - const double* ar_ptr = a; - const double* ai_ptr = a + m; - const double* br_ptr = b; - const double* bi_ptr = b + m; - - const double* const rend_ptr = ri_ptr; - while (rr_ptr != rend_ptr) { - const __m256d ar = _mm256_loadu_pd(ar_ptr); - const __m256d ai = _mm256_loadu_pd(ai_ptr); - const __m256d br = _mm256_loadu_pd(br_ptr); - const __m256d bi = _mm256_loadu_pd(bi_ptr); - - __m256d rr = _mm256_sub_pd(ar, br); - __m256d ri = _mm256_sub_pd(ai, bi); - - _mm256_storeu_pd(rr_ptr, rr); - _mm256_storeu_pd(ri_ptr, ri); - - rr_ptr += 4; - ri_ptr += 4; - ar_ptr += 4; - ai_ptr += 4; - br_ptr += 4; - bi_ptr += 4; - } -}
@@ -1,242 +0,0 @@
-#include <string.h> - -#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -// Computes X^i -> X^(p*i) in the fourier domain for a reim vector is size 2 * m * size -// This function cannot be evaluated in place. -EXPORT void reim_fftvec_automorphism_ref(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* tables, int64_t p, double* r, - const double* a, uint64_t a_size) { - const uint64_t m = tables->m; - const uint64_t nn = 2 * m; - const uint64_t* irev = tables->irev; - const uint64_t mask = (4 * m - 1); - const uint64_t conj = !((p & 3) == 1); - p = p & mask; - p *= 1 - (conj << 1); - if (a_size == 1) { - for (uint64_t i = 0; i < m; ++i) { - uint64_t i_rev = 2 * irev[i] + 1; - i_rev = (((p * i_rev) & mask) - 1) >> 1; - uint64_t j = irev[i_rev]; - r[i] = a[j]; - double x = a[j + m]; - r[i + m] = conj ? -x : x; - } - } else { - for (uint64_t i = 0; i < m; ++i) { - uint64_t i_rev = 2 * irev[i] + 1; - i_rev = (((p * i_rev) & mask) - 1) >> 1; - uint64_t j = irev[i_rev]; - for (uint64_t k = 0; k < a_size; ++k) { - uint64_t offset_re = k * nn; - uint64_t offset_im = offset_re + m; - r[i + offset_re] = a[j + offset_re]; - double x = a[j + offset_im]; - r[i + offset_im] = conj ? -x : x; - } - } - } -} - -// Computes the permutation index for an automorphism X^{i} -> X^{i*p} in the fourier domain. -EXPORT void reim_fftvec_automorphism_lut_precomp_ref(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* tables, int64_t p, - uint64_t* precomp // size m -) { - const uint64_t m = tables->m; - const uint64_t* irev = tables->irev; - const uint64_t mask = (4 * m - 1); - const uint64_t conj = !((p & 3) == 1); - p = p & mask; - p *= 1 - (conj << 1); - for (uint64_t i = 0; i < m; ++i) { - uint64_t i_rev = 2 * irev[i] + 1; - i_rev = (((p * i_rev) & mask) - 1) >> 1; - uint64_t j = irev[i_rev]; - precomp[i] = j; - } -} - -// Computes X^{i} -> X^{i*p} in the fourier domain for a reim vector of size m using a precomputed lut permutation. -void reim_fftvec_automorphism_with_lut_ref(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* tables, uint64_t* precomp, double* r, - const double* a) { - const uint64_t m = tables->m; - for (uint64_t i = 0; i < m; ++i) { - uint64_t j = precomp[i]; - r[i] = a[j]; - r[i + m] = a[j + m]; - } -} - -// Computes X^{i} -> X^{i*-p} in the fourier domain for a reim vector of size m using a precomputed lut permutation. -void reim_fftvec_automorphism_conj_with_lut_ref(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* tables, uint64_t* precomp, - double* r, const double* a) { - const uint64_t m = tables->m; - for (uint64_t i = 0; i < m; ++i) { - uint64_t j = precomp[i]; - r[i] = a[j]; - r[i + m] = -a[j + m]; - } -} - -// Returns the minimum number of temporary bytes used by reim_fftvec_automorphism_inplace_ref. -EXPORT uint64_t reim_fftvec_automorphism_inplace_tmp_bytes_ref(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* tables) { - const uint64_t m = tables->m; - return m * (2 * sizeof(double) + sizeof(uint64_t)); -} - -// Computes X^i -> X^(p*i) in the fourier domain for a reim vector is size 2 * m * a_size -// This function cannot be evaluated in place. -EXPORT void reim_fftvec_automorphism_inplace_ref(const REIM_FFTVEC_AUTOMORPHISM_PRECOMP* tables, int64_t p, double* a, - uint64_t a_size, - uint8_t* tmp_bytes // m * (2*sizeof(double) + sizeof(uint64_t)) -) { - const uint64_t m = tables->m; - const uint64_t nn = 2 * m; - const uint64_t* irev = tables->irev; - const uint64_t mask = (4 * m - 1); - const uint64_t conj = !((p & 3) == 1); - - double* tmp = (double*)tmp_bytes; - p = p & mask; - if (a_size == 1) { - p *= 1 - (conj << 1); - for (uint64_t i = 0; i < m; ++i) { - uint64_t i_rev = 2 * irev[i] + 1; - i_rev = (((p * i_rev) & mask) - 1) >> 1; - uint64_t j = irev[i_rev]; - tmp[i] = a[j]; - double x = a[j + m]; - tmp[i + m] = conj ? -x : x; - } - memcpy(a, tmp, nn * sizeof(double)); - } else { - uint64_t* lut = (uint64_t*)(tmp_bytes + nn * sizeof(double)); - reim_fftvec_automorphism_lut_precomp_ref(tables, p, lut); - for (uint64_t i = 0; i < a_size; ++i) { - if (conj == 1) { - reim_fftvec_automorphism_conj_with_lut_ref(tables, lut, tmp, a + i * nn); - } else { - reim_fftvec_automorphism_with_lut_ref(tables, lut, tmp, a + i * nn); - } - memcpy(a + i * nn, tmp, nn * sizeof(double)); - } - } -} - -EXPORT void reim_fftvec_addmul_ref(const REIM_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, - const double* b) { - const uint64_t m = tables->m; - for (uint64_t i = 0; i < m; ++i) { - double re = a[i] * b[i] - a[i + m] * b[i + m]; - double im = a[i] * b[i + m] + a[i + m] * b[i]; - r[i] += re; - r[i + m] += im; - } -} - -EXPORT void reim_fftvec_add_ref(const REIM_FFTVEC_ADD_PRECOMP* precomp, double* r, const double* a, const double* b) { - const uint64_t m = precomp->m; - for (uint64_t i = 0; i < m; ++i) { - double re = a[i] + b[i]; - double im = a[i + m] + b[i + m]; - r[i] = re; - r[i + m] = im; - } -} - -EXPORT void reim_fftvec_sub_ref(const REIM_FFTVEC_SUB_PRECOMP* precomp, double* r, const double* a, const double* b) { - const uint64_t m = precomp->m; - for (uint64_t i = 0; i < m; ++i) { - double re = a[i] - b[i]; - double im = a[i + m] - b[i + m]; - r[i] = re; - r[i + m] = im; - } -} - -EXPORT void reim_fftvec_mul_ref(const REIM_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b) { - const uint64_t m = tables->m; - for (uint64_t i = 0; i < m; ++i) { - double re = a[i] * b[i] - a[i + m] * b[i + m]; - double im = a[i] * b[i + m] + a[i + m] * b[i]; - r[i] = re; - r[i + m] = im; - } -} - -EXPORT REIM_FFTVEC_ADDMUL_PRECOMP* new_reim_fftvec_addmul_precomp(uint32_t m) { - REIM_FFTVEC_ADDMUL_PRECOMP* reps = malloc(sizeof(REIM_FFTVEC_ADDMUL_PRECOMP)); - reps->m = m; - if (CPU_SUPPORTS("fma")) { - if (m >= 4) { - reps->function = reim_fftvec_addmul_fma; - } else { - reps->function = reim_fftvec_addmul_ref; - } - } else { - reps->function = reim_fftvec_addmul_ref; - } - return reps; -} - -EXPORT REIM_FFTVEC_ADD_PRECOMP* new_reim_fftvec_add_precomp(uint32_t m) { - REIM_FFTVEC_ADD_PRECOMP* reps = malloc(sizeof(REIM_FFTVEC_ADD_PRECOMP)); - reps->m = m; - if (CPU_SUPPORTS("fma")) { - if (m >= 4) { - reps->function = reim_fftvec_add_fma; - } else { - reps->function = reim_fftvec_add_ref; - } - } else { - reps->function = reim_fftvec_add_ref; - } - return reps; -} - -EXPORT REIM_FFTVEC_SUB_PRECOMP* new_reim_fftvec_sub_precomp(uint32_t m) { - REIM_FFTVEC_SUB_PRECOMP* reps = malloc(sizeof(REIM_FFTVEC_SUB_PRECOMP)); - reps->m = m; - if (CPU_SUPPORTS("fma")) { - if (m >= 4) { - reps->function = reim_fftvec_sub_fma; - } else { - reps->function = reim_fftvec_sub_ref; - } - } else { - reps->function = reim_fftvec_sub_ref; - } - return reps; -} - -EXPORT REIM_FFTVEC_MUL_PRECOMP* new_reim_fftvec_mul_precomp(uint32_t m) { - REIM_FFTVEC_MUL_PRECOMP* reps = malloc(sizeof(REIM_FFTVEC_MUL_PRECOMP)); - reps->m = m; - if (CPU_SUPPORTS("fma")) { - if (m >= 4) { - reps->function = reim_fftvec_mul_fma; - } else { - reps->function = reim_fftvec_mul_ref; - } - } else { - reps->function = reim_fftvec_mul_ref; - } - return reps; -} - -EXPORT REIM_FFTVEC_AUTOMORPHISM_PRECOMP* new_reim_fftvec_automorphism_precomp(uint32_t m) { - REIM_FFTVEC_AUTOMORPHISM_PRECOMP* reps = malloc(sizeof(REIM_FFTVEC_AUTOMORPHISM_PRECOMP)); - reps->m = m; - reps->function.apply = reim_fftvec_automorphism_ref; - reps->function.apply_inplace = reim_fftvec_automorphism_inplace_ref; - reps->function.apply_inplace_tmp_bytes = reim_fftvec_automorphism_inplace_tmp_bytes_ref; - const uint64_t nn = 2 * m; - reps->irev = malloc(sizeof(uint64_t) * nn); - uint32_t lognn = log2m(nn); - for (uint32_t i = 0; i < nn; i++) { - reps->irev[i] = (uint64_t)revbits(lognn, i); - } - return reps; -}
@@ -1,192 +0,0 @@
-#rdi datare ptr -#rsi dataim ptr -#rdx om ptr -.globl reim_ifft16_avx_fma -reim_ifft16_avx_fma: -vmovupd (%rdi),%ymm0 # ra0 -vmovupd 0x20(%rdi),%ymm1 # ra4 -vmovupd 0x40(%rdi),%ymm2 # ra8 -vmovupd 0x60(%rdi),%ymm3 # ra12 -vmovupd (%rsi),%ymm4 # ia0 -vmovupd 0x20(%rsi),%ymm5 # ia4 -vmovupd 0x40(%rsi),%ymm6 # ia8 -vmovupd 0x60(%rsi),%ymm7 # ia12 - -1: -vmovupd 0x00(%rdx),%ymm12 -vmovupd 0x20(%rdx),%ymm13 - -vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) - -vunpckhpd %ymm1,%ymm0,%ymm4 # (0,1) -> (0,4) -vunpckhpd %ymm3,%ymm2,%ymm6 # (2,3) -> (2,6) -vunpckhpd %ymm9,%ymm8,%ymm5 # (8,9) -> (1,5) -vunpckhpd %ymm11,%ymm10,%ymm7 # (10,11) -> (3,7) -vunpcklpd %ymm1,%ymm0,%ymm0 -vunpcklpd %ymm3,%ymm2,%ymm2 -vunpcklpd %ymm9,%ymm8,%ymm1 -vunpcklpd %ymm11,%ymm10,%ymm3 - -# invctwiddle Re:(ymm0,ymm4) and Im:(ymm2,ymm6) with omega=(ymm12,ymm13) -# invcitwiddle Re:(ymm1,ymm5) and Im:(ymm3,ymm7) with omega=(ymm12,ymm13) -vsubpd %ymm4,%ymm0,%ymm8 # retw -vsubpd %ymm5,%ymm1,%ymm9 # reitw -vsubpd %ymm6,%ymm2,%ymm10 # imtw -vsubpd %ymm7,%ymm3,%ymm11 # imitw -vaddpd %ymm4,%ymm0,%ymm0 -vaddpd %ymm5,%ymm1,%ymm1 -vaddpd %ymm6,%ymm2,%ymm2 -vaddpd %ymm7,%ymm3,%ymm3 -# multiply 8,9,10,11 by 12,13, result to: 4,5,6,7 -# twiddles use reom=ymm12, imom=ymm13 -# invtwiddles use reom=ymm13, imom=-ymm12 -vmulpd %ymm10,%ymm13,%ymm4 # imtw.omai (tw) -vmulpd %ymm11,%ymm12,%ymm5 # imitw.omar (itw) -vmulpd %ymm8,%ymm13,%ymm6 # retw.omai (tw) -vmulpd %ymm9,%ymm12,%ymm7 # reitw.omar (itw) -vfmsub231pd %ymm8,%ymm12,%ymm4 # rprod0 (tw) -vfmadd231pd %ymm9,%ymm13,%ymm5 # rprod4 (itw) -vfmadd231pd %ymm10,%ymm12,%ymm6 # iprod0 (tw) -vfmsub231pd %ymm11,%ymm13,%ymm7 # iprod4 (itw) - -vunpckhpd %ymm7,%ymm3,%ymm11 # (0,4) -> (0,1) -vunpckhpd %ymm5,%ymm1,%ymm9 # (2,6) -> (2,3) -vunpcklpd %ymm7,%ymm3,%ymm10 -vunpcklpd %ymm5,%ymm1,%ymm8 -vunpckhpd %ymm6,%ymm2,%ymm3 # (1,5) -> (8,9) -vunpckhpd %ymm4,%ymm0,%ymm1 # (3,7) -> (10,11) -vunpcklpd %ymm6,%ymm2,%ymm2 -vunpcklpd %ymm4,%ymm0,%ymm0 - -/* -vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 -*/ -2: -vmovupd 0x40(%rdx),%ymm12 -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omaiii'i' -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omarrr'r' - -/* -vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) -*/ - -# invctwiddle Re:(ymm0,ymm8) and Im:(ymm2,ymm10) with omega=(ymm12,ymm13) -# invcitwiddle Re:(ymm1,ymm9) and Im:(ymm3,ymm11) with omega=(ymm12,ymm13) -vsubpd %ymm8,%ymm0,%ymm4 # retw -vsubpd %ymm9,%ymm1,%ymm5 # reitw -vsubpd %ymm10,%ymm2,%ymm6 # imtw -vsubpd %ymm11,%ymm3,%ymm7 # imitw -vaddpd %ymm8,%ymm0,%ymm0 -vaddpd %ymm9,%ymm1,%ymm1 -vaddpd %ymm10,%ymm2,%ymm2 -vaddpd %ymm11,%ymm3,%ymm3 -# multiply 4,5,6,7 by 12,13, result to 8,9,10,11 -# twiddles use reom=ymm12, imom=ymm13 -# invtwiddles use reom=ymm13, imom=-ymm12 -vmulpd %ymm6,%ymm13,%ymm8 # imtw.omai (tw) -vmulpd %ymm7,%ymm12,%ymm9 # imitw.omar (itw) -vmulpd %ymm4,%ymm13,%ymm10 # retw.omai (tw) -vmulpd %ymm5,%ymm12,%ymm11 # reitw.omar (itw) -vfmsub231pd %ymm4,%ymm12,%ymm8 # rprod0 (tw) -vfmadd231pd %ymm5,%ymm13,%ymm9 # rprod4 (itw) -vfmadd231pd %ymm6,%ymm12,%ymm10 # iprod0 (tw) -vfmsub231pd %ymm7,%ymm13,%ymm11 # iprod4 (itw) - -vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 - -3: -vmovupd 0x60(%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar - -# invctwiddle Re:(ymm0,ymm1) and Im:(ymm4,ymm5) with omega=(ymm12,ymm13) -# invcitwiddle Re:(ymm2,ymm3) and Im:(ymm6,ymm7) with omega=(ymm12,ymm13) -vsubpd %ymm1,%ymm0,%ymm8 # retw -vsubpd %ymm3,%ymm2,%ymm9 # reitw -vsubpd %ymm5,%ymm4,%ymm10 # imtw -vsubpd %ymm7,%ymm6,%ymm11 # imitw -vaddpd %ymm1,%ymm0,%ymm0 -vaddpd %ymm3,%ymm2,%ymm2 -vaddpd %ymm5,%ymm4,%ymm4 -vaddpd %ymm7,%ymm6,%ymm6 -# multiply 8,9,10,11 by 12,13, result to 1,3,5,7 -# twiddles use reom=ymm12, imom=ymm13 -# invtwiddles use reom=ymm13, imom=-ymm12 -vmulpd %ymm10,%ymm13,%ymm1 # imtw.omai (tw) -vmulpd %ymm11,%ymm12,%ymm3 # imitw.omar (itw) -vmulpd %ymm8,%ymm13,%ymm5 # retw.omai (tw) -vmulpd %ymm9,%ymm12,%ymm7 # reitw.omar (itw) -vfmsub231pd %ymm8,%ymm12,%ymm1 # rprod0 (tw) -vfmadd231pd %ymm9,%ymm13,%ymm3 # rprod4 (itw) -vfmadd231pd %ymm10,%ymm12,%ymm5 # iprod0 (tw) -vfmsub231pd %ymm11,%ymm13,%ymm7 # iprod4 (itw) - -4: -vmovupd 0x70(%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar - -# invctwiddle Re:(ymm0,ymm2) and Im:(ymm4,ymm6) with omega=(ymm12,ymm13) -# invctwiddle Re:(ymm1,ymm3) and Im:(ymm5,ymm7) with omega=(ymm12,ymm13) -vsubpd %ymm2,%ymm0,%ymm8 # retw1 -vsubpd %ymm3,%ymm1,%ymm9 # retw2 -vsubpd %ymm6,%ymm4,%ymm10 # imtw1 -vsubpd %ymm7,%ymm5,%ymm11 # imtw2 -vaddpd %ymm2,%ymm0,%ymm0 -vaddpd %ymm3,%ymm1,%ymm1 -vaddpd %ymm6,%ymm4,%ymm4 -vaddpd %ymm7,%ymm5,%ymm5 -# multiply 8,9,10,11 by 12,13, result to 2,3,6,7 -# twiddles use reom=ymm12, imom=ymm13 -vmulpd %ymm10,%ymm13,%ymm2 # imtw1.omai -vmulpd %ymm11,%ymm13,%ymm3 # imtw2.omai -vmulpd %ymm8,%ymm13,%ymm6 # retw1.omai -vmulpd %ymm9,%ymm13,%ymm7 # retw2.omai -vfmsub231pd %ymm8,%ymm12,%ymm2 # rprod0 -vfmsub231pd %ymm9,%ymm12,%ymm3 # rprod4 -vfmadd231pd %ymm10,%ymm12,%ymm6 # iprod0 -vfmadd231pd %ymm11,%ymm12,%ymm7 # iprod4 - -5: -vmovupd %ymm0,(%rdi) # ra0 -vmovupd %ymm1,0x20(%rdi) # ra4 -vmovupd %ymm2,0x40(%rdi) # ra8 -vmovupd %ymm3,0x60(%rdi) # ra12 -vmovupd %ymm4,(%rsi) # ia0 -vmovupd %ymm5,0x20(%rsi) # ia4 -vmovupd %ymm6,0x40(%rsi) # ia8 -vmovupd %ymm7,0x60(%rsi) # ia12 -vzeroupper -ret -.size reim_ifft16_avx_fma, .-reim_ifft16_avx_fma -.section .note.GNU-stack,"",@progbits
@@ -1,228 +0,0 @@
- .text - .p2align 4 - .globl reim_ifft16_avx_fma - .def reim_ifft16_avx_fma; .scl 2; .type 32; .endef -reim_ifft16_avx_fma: - - pushq %rdi - pushq %rsi - movq %rcx,%rdi - movq %rdx,%rsi - movq %r8,%rdx - subq $0x100,%rsp - movdqu %xmm6,(%rsp) - movdqu %xmm7,0x10(%rsp) - movdqu %xmm8,0x20(%rsp) - movdqu %xmm9,0x30(%rsp) - movdqu %xmm10,0x40(%rsp) - movdqu %xmm11,0x50(%rsp) - movdqu %xmm12,0x60(%rsp) - movdqu %xmm13,0x70(%rsp) - movdqu %xmm14,0x80(%rsp) - movdqu %xmm15,0x90(%rsp) - callq reim_ifft16_avx_fma_amd64 - movdqu (%rsp),%xmm6 - movdqu 0x10(%rsp),%xmm7 - movdqu 0x20(%rsp),%xmm8 - movdqu 0x30(%rsp),%xmm9 - movdqu 0x40(%rsp),%xmm10 - movdqu 0x50(%rsp),%xmm11 - movdqu 0x60(%rsp),%xmm12 - movdqu 0x70(%rsp),%xmm13 - movdqu 0x80(%rsp),%xmm14 - movdqu 0x90(%rsp),%xmm15 - addq $0x100,%rsp - popq %rsi - popq %rdi - retq - -#rdi datare ptr -#rsi dataim ptr -#rdx om ptr -#.globl reim_ifft16_avx_fma_amd64 -reim_ifft16_avx_fma_amd64: -vmovupd (%rdi),%ymm0 # ra0 -vmovupd 0x20(%rdi),%ymm1 # ra4 -vmovupd 0x40(%rdi),%ymm2 # ra8 -vmovupd 0x60(%rdi),%ymm3 # ra12 -vmovupd (%rsi),%ymm4 # ia0 -vmovupd 0x20(%rsi),%ymm5 # ia4 -vmovupd 0x40(%rsi),%ymm6 # ia8 -vmovupd 0x60(%rsi),%ymm7 # ia12 - -1: -vmovupd 0x00(%rdx),%ymm12 -vmovupd 0x20(%rdx),%ymm13 - -vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) - -vunpckhpd %ymm1,%ymm0,%ymm4 # (0,1) -> (0,4) -vunpckhpd %ymm3,%ymm2,%ymm6 # (2,3) -> (2,6) -vunpckhpd %ymm9,%ymm8,%ymm5 # (8,9) -> (1,5) -vunpckhpd %ymm11,%ymm10,%ymm7 # (10,11) -> (3,7) -vunpcklpd %ymm1,%ymm0,%ymm0 -vunpcklpd %ymm3,%ymm2,%ymm2 -vunpcklpd %ymm9,%ymm8,%ymm1 -vunpcklpd %ymm11,%ymm10,%ymm3 - -# invctwiddle Re:(ymm0,ymm4) and Im:(ymm2,ymm6) with omega=(ymm12,ymm13) -# invcitwiddle Re:(ymm1,ymm5) and Im:(ymm3,ymm7) with omega=(ymm12,ymm13) -vsubpd %ymm4,%ymm0,%ymm8 # retw -vsubpd %ymm5,%ymm1,%ymm9 # reitw -vsubpd %ymm6,%ymm2,%ymm10 # imtw -vsubpd %ymm7,%ymm3,%ymm11 # imitw -vaddpd %ymm4,%ymm0,%ymm0 -vaddpd %ymm5,%ymm1,%ymm1 -vaddpd %ymm6,%ymm2,%ymm2 -vaddpd %ymm7,%ymm3,%ymm3 -# multiply 8,9,10,11 by 12,13, result to: 4,5,6,7 -# twiddles use reom=ymm12, imom=ymm13 -# invtwiddles use reom=ymm13, imom=-ymm12 -vmulpd %ymm10,%ymm13,%ymm4 # imtw.omai (tw) -vmulpd %ymm11,%ymm12,%ymm5 # imitw.omar (itw) -vmulpd %ymm8,%ymm13,%ymm6 # retw.omai (tw) -vmulpd %ymm9,%ymm12,%ymm7 # reitw.omar (itw) -vfmsub231pd %ymm8,%ymm12,%ymm4 # rprod0 (tw) -vfmadd231pd %ymm9,%ymm13,%ymm5 # rprod4 (itw) -vfmadd231pd %ymm10,%ymm12,%ymm6 # iprod0 (tw) -vfmsub231pd %ymm11,%ymm13,%ymm7 # iprod4 (itw) - -vunpckhpd %ymm7,%ymm3,%ymm11 # (0,4) -> (0,1) -vunpckhpd %ymm5,%ymm1,%ymm9 # (2,6) -> (2,3) -vunpcklpd %ymm7,%ymm3,%ymm10 -vunpcklpd %ymm5,%ymm1,%ymm8 -vunpckhpd %ymm6,%ymm2,%ymm3 # (1,5) -> (8,9) -vunpckhpd %ymm4,%ymm0,%ymm1 # (3,7) -> (10,11) -vunpcklpd %ymm6,%ymm2,%ymm2 -vunpcklpd %ymm4,%ymm0,%ymm0 - -/* -vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 -*/ -2: -vmovupd 0x40(%rdx),%ymm12 -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omaiii'i' -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omarrr'r' - -/* -vperm2f128 $0x31,%ymm2,%ymm0,%ymm8 # ymm8 contains re to mul (tw) -vperm2f128 $0x31,%ymm3,%ymm1,%ymm9 # ymm9 contains re to mul (itw) -vperm2f128 $0x31,%ymm6,%ymm4,%ymm10 # ymm10 contains im to mul (tw) -vperm2f128 $0x31,%ymm7,%ymm5,%ymm11 # ymm11 contains im to mul (itw) -vperm2f128 $0x20,%ymm2,%ymm0,%ymm0 # ymm0 contains re to add (tw) -vperm2f128 $0x20,%ymm3,%ymm1,%ymm1 # ymm1 contains re to add (itw) -vperm2f128 $0x20,%ymm6,%ymm4,%ymm2 # ymm2 contains im to add (tw) -vperm2f128 $0x20,%ymm7,%ymm5,%ymm3 # ymm3 contains im to add (itw) -*/ - -# invctwiddle Re:(ymm0,ymm8) and Im:(ymm2,ymm10) with omega=(ymm12,ymm13) -# invcitwiddle Re:(ymm1,ymm9) and Im:(ymm3,ymm11) with omega=(ymm12,ymm13) -vsubpd %ymm8,%ymm0,%ymm4 # retw -vsubpd %ymm9,%ymm1,%ymm5 # reitw -vsubpd %ymm10,%ymm2,%ymm6 # imtw -vsubpd %ymm11,%ymm3,%ymm7 # imitw -vaddpd %ymm8,%ymm0,%ymm0 -vaddpd %ymm9,%ymm1,%ymm1 -vaddpd %ymm10,%ymm2,%ymm2 -vaddpd %ymm11,%ymm3,%ymm3 -# multiply 4,5,6,7 by 12,13, result to 8,9,10,11 -# twiddles use reom=ymm12, imom=ymm13 -# invtwiddles use reom=ymm13, imom=-ymm12 -vmulpd %ymm6,%ymm13,%ymm8 # imtw.omai (tw) -vmulpd %ymm7,%ymm12,%ymm9 # imitw.omar (itw) -vmulpd %ymm4,%ymm13,%ymm10 # retw.omai (tw) -vmulpd %ymm5,%ymm12,%ymm11 # reitw.omar (itw) -vfmsub231pd %ymm4,%ymm12,%ymm8 # rprod0 (tw) -vfmadd231pd %ymm5,%ymm13,%ymm9 # rprod4 (itw) -vfmadd231pd %ymm6,%ymm12,%ymm10 # iprod0 (tw) -vfmsub231pd %ymm7,%ymm13,%ymm11 # iprod4 (itw) - -vperm2f128 $0x31,%ymm10,%ymm2,%ymm6 -vperm2f128 $0x31,%ymm11,%ymm3,%ymm7 -vperm2f128 $0x20,%ymm10,%ymm2,%ymm4 -vperm2f128 $0x20,%ymm11,%ymm3,%ymm5 -vperm2f128 $0x31,%ymm8,%ymm0,%ymm2 -vperm2f128 $0x31,%ymm9,%ymm1,%ymm3 -vperm2f128 $0x20,%ymm8,%ymm0,%ymm0 -vperm2f128 $0x20,%ymm9,%ymm1,%ymm1 - -3: -vmovupd 0x60(%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar - -# invctwiddle Re:(ymm0,ymm1) and Im:(ymm4,ymm5) with omega=(ymm12,ymm13) -# invcitwiddle Re:(ymm2,ymm3) and Im:(ymm6,ymm7) with omega=(ymm12,ymm13) -vsubpd %ymm1,%ymm0,%ymm8 # retw -vsubpd %ymm3,%ymm2,%ymm9 # reitw -vsubpd %ymm5,%ymm4,%ymm10 # imtw -vsubpd %ymm7,%ymm6,%ymm11 # imitw -vaddpd %ymm1,%ymm0,%ymm0 -vaddpd %ymm3,%ymm2,%ymm2 -vaddpd %ymm5,%ymm4,%ymm4 -vaddpd %ymm7,%ymm6,%ymm6 -# multiply 8,9,10,11 by 12,13, result to 1,3,5,7 -# twiddles use reom=ymm12, imom=ymm13 -# invtwiddles use reom=ymm13, imom=-ymm12 -vmulpd %ymm10,%ymm13,%ymm1 # imtw.omai (tw) -vmulpd %ymm11,%ymm12,%ymm3 # imitw.omar (itw) -vmulpd %ymm8,%ymm13,%ymm5 # retw.omai (tw) -vmulpd %ymm9,%ymm12,%ymm7 # reitw.omar (itw) -vfmsub231pd %ymm8,%ymm12,%ymm1 # rprod0 (tw) -vfmadd231pd %ymm9,%ymm13,%ymm3 # rprod4 (itw) -vfmadd231pd %ymm10,%ymm12,%ymm5 # iprod0 (tw) -vfmsub231pd %ymm11,%ymm13,%ymm7 # iprod4 (itw) - -4: -vmovupd 0x70(%rdx),%xmm12 -vinsertf128 $1, %xmm12, %ymm12, %ymm12 # omriri -vshufpd $15, %ymm12, %ymm12, %ymm13 # ymm13: omai -vshufpd $0, %ymm12, %ymm12, %ymm12 # ymm12: omar - -# invctwiddle Re:(ymm0,ymm2) and Im:(ymm4,ymm6) with omega=(ymm12,ymm13) -# invctwiddle Re:(ymm1,ymm3) and Im:(ymm5,ymm7) with omega=(ymm12,ymm13) -vsubpd %ymm2,%ymm0,%ymm8 # retw1 -vsubpd %ymm3,%ymm1,%ymm9 # retw2 -vsubpd %ymm6,%ymm4,%ymm10 # imtw1 -vsubpd %ymm7,%ymm5,%ymm11 # imtw2 -vaddpd %ymm2,%ymm0,%ymm0 -vaddpd %ymm3,%ymm1,%ymm1 -vaddpd %ymm6,%ymm4,%ymm4 -vaddpd %ymm7,%ymm5,%ymm5 -# multiply 8,9,10,11 by 12,13, result to 2,3,6,7 -# twiddles use reom=ymm12, imom=ymm13 -vmulpd %ymm10,%ymm13,%ymm2 # imtw1.omai -vmulpd %ymm11,%ymm13,%ymm3 # imtw2.omai -vmulpd %ymm8,%ymm13,%ymm6 # retw1.omai -vmulpd %ymm9,%ymm13,%ymm7 # retw2.omai -vfmsub231pd %ymm8,%ymm12,%ymm2 # rprod0 -vfmsub231pd %ymm9,%ymm12,%ymm3 # rprod4 -vfmadd231pd %ymm10,%ymm12,%ymm6 # iprod0 -vfmadd231pd %ymm11,%ymm12,%ymm7 # iprod4 - -5: -vmovupd %ymm0,(%rdi) # ra0 -vmovupd %ymm1,0x20(%rdi) # ra4 -vmovupd %ymm2,0x40(%rdi) # ra8 -vmovupd %ymm3,0x60(%rdi) # ra12 -vmovupd %ymm4,(%rsi) # ia0 -vmovupd %ymm5,0x20(%rsi) # ia4 -vmovupd %ymm6,0x40(%rsi) # ia8 -vmovupd %ymm7,0x60(%rsi) # ia12 -vzeroupper -ret
@@ -1,62 +0,0 @@
-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim_fft_private.h" - -__always_inline void reim_invctwiddle_avx_fma(__m128d* ra, __m128d* rb, __m128d* ia, __m128d* ib, const __m128d omre, - const __m128d omim) { - __m128d rdiff = _mm_sub_pd(*ra, *rb); - __m128d idiff = _mm_sub_pd(*ia, *ib); - *ra = _mm_add_pd(*ra, *rb); - *ia = _mm_add_pd(*ia, *ib); - - *rb = _mm_mul_pd(idiff, omim); - *rb = _mm_fmsub_pd(rdiff, omre, *rb); - - *ib = _mm_mul_pd(rdiff, omim); - *ib = _mm_fmadd_pd(idiff, omre, *ib); -} - -EXPORT void reim_ifft4_avx_fma(double* dre, double* dim, const void* ompv) { - const double* omp = (const double*)ompv; - - __m128d ra0 = _mm_loadu_pd(dre); - __m128d ra2 = _mm_loadu_pd(dre + 2); - __m128d ia0 = _mm_loadu_pd(dim); - __m128d ia2 = _mm_loadu_pd(dim + 2); - - // 1 - { - const __m128d ifft4neg = _mm_castsi128_pd(_mm_set_epi64x(UINT64_C(1) << 63, 0)); - __m128d omre = _mm_loadu_pd(omp); // omre: r,i - __m128d omim = _mm_xor_pd(_mm_permute_pd(omre, 1), ifft4neg); // omim: i,-r - - __m128d ra = _mm_unpacklo_pd(ra0, ra2); // (r0, r1), (r2, r3) -> (r0, r2) - __m128d ia = _mm_unpacklo_pd(ia0, ia2); // (i0, i1), (i2, i3) -> (i0, i2) - __m128d rb = _mm_unpackhi_pd(ra0, ra2); // (r0, r1), (r2, r3) -> (r1, r3) - __m128d ib = _mm_unpackhi_pd(ia0, ia2); // (i0, i1), (i2, i3) -> (i1, i3) - - reim_invctwiddle_avx_fma(&ra, &rb, &ia, &ib, omre, omim); - - ra0 = _mm_unpacklo_pd(ra, rb); - ia0 = _mm_unpacklo_pd(ia, ib); - ra2 = _mm_unpackhi_pd(ra, rb); - ia2 = _mm_unpackhi_pd(ia, ib); - } - - // 2 - { - __m128d om = _mm_loadu_pd(omp + 2); - __m128d omre = _mm_permute_pd(om, 0); - __m128d omim = _mm_permute_pd(om, 3); - - reim_invctwiddle_avx_fma(&ra0, &ra2, &ia0, &ia2, omre, omim); - } - - // 4 - _mm_storeu_pd(dre, ra0); - _mm_storeu_pd(dre + 2, ra2); - _mm_storeu_pd(dim, ia0); - _mm_storeu_pd(dim + 2, ia2); -}
@@ -1,86 +0,0 @@
-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim_fft_private.h" - -__always_inline void reim_invctwiddle_avx_fma(__m256d* ra, __m256d* rb, __m256d* ia, __m256d* ib, const __m256d omre, - const __m256d omim) { - __m256d rdiff = _mm256_sub_pd(*ra, *rb); - __m256d idiff = _mm256_sub_pd(*ia, *ib); - *ra = _mm256_add_pd(*ra, *rb); - *ia = _mm256_add_pd(*ia, *ib); - - *rb = _mm256_mul_pd(idiff, omim); - *rb = _mm256_fmsub_pd(rdiff, omre, *rb); - - *ib = _mm256_mul_pd(rdiff, omim); - *ib = _mm256_fmadd_pd(idiff, omre, *ib); -} - -EXPORT void reim_ifft8_avx_fma(double* dre, double* dim, const void* ompv) { - const double* omp = (const double*)ompv; - - __m256d ra0 = _mm256_loadu_pd(dre); - __m256d ra4 = _mm256_loadu_pd(dre + 4); - __m256d ia0 = _mm256_loadu_pd(dim); - __m256d ia4 = _mm256_loadu_pd(dim + 4); - - // 1 - { - const __m256d fft8neg2 = _mm256_castsi256_pd(_mm256_set_epi64x(UINT64_C(1) << 63, UINT64_C(1) << 63, 0, 0)); - __m256d omr = _mm256_loadu_pd(omp); // r0,r1,i0,i1 - __m256d omiirr = _mm256_permute2f128_pd(omr, omr, 1); // i0,i1,r0,r1 - __m256d omi = _mm256_xor_pd(omiirr, fft8neg2); // i0,i1,-r0,-r1 - - __m256d rb = _mm256_unpackhi_pd(ra0, ra4); - __m256d ib = _mm256_unpackhi_pd(ia0, ia4); - __m256d ra = _mm256_unpacklo_pd(ra0, ra4); - __m256d ia = _mm256_unpacklo_pd(ia0, ia4); - - reim_invctwiddle_avx_fma(&ra, &rb, &ia, &ib, omr, omi); - - ra4 = _mm256_unpackhi_pd(ra, rb); - ia4 = _mm256_unpackhi_pd(ia, ib); - ra0 = _mm256_unpacklo_pd(ra, rb); - ia0 = _mm256_unpacklo_pd(ia, ib); - } - - // 2 - { - const __m128d ifft8neg = _mm_castsi128_pd(_mm_set_epi64x(0, UINT64_C(1) << 63)); - __m128d omri = _mm_loadu_pd(omp + 4); // r,i - __m128d ommri = _mm_xor_pd(omri, ifft8neg); // -r,i - __m256d omrimri = _mm256_set_m128d(ommri, omri); // r,i,-r,i - __m256d omi = _mm256_permute_pd(omrimri, 3); // i,i,-r,-r - __m256d omr = _mm256_permute_pd(omrimri, 12); // r,r,i,i - - __m256d rb = _mm256_permute2f128_pd(ra0, ra4, 0x31); - __m256d ib = _mm256_permute2f128_pd(ia0, ia4, 0x31); - __m256d ra = _mm256_permute2f128_pd(ra0, ra4, 0x20); - __m256d ia = _mm256_permute2f128_pd(ia0, ia4, 0x20); - - reim_invctwiddle_avx_fma(&ra, &rb, &ia, &ib, omr, omi); - - ra0 = _mm256_permute2f128_pd(ra, rb, 0x20); - ra4 = _mm256_permute2f128_pd(ra, rb, 0x31); - ia0 = _mm256_permute2f128_pd(ia, ib, 0x20); - ia4 = _mm256_permute2f128_pd(ia, ib, 0x31); - } - - // 3 - { - __m128d omri = _mm_loadu_pd(omp + 6); // r,i - __m256d omriri = _mm256_set_m128d(omri, omri); // r,i,r,i - __m256d omi = _mm256_permute_pd(omriri, 15); // i,i,i,i - __m256d omr = _mm256_permute_pd(omriri, 0); // r,r,r,r - - reim_invctwiddle_avx_fma(&ra0, &ra4, &ia0, &ia4, omr, omi); - } - - // 4 - _mm256_storeu_pd(dre, ra0); - _mm256_storeu_pd(dre + 4, ra4); - _mm256_storeu_pd(dim, ia0); - _mm256_storeu_pd(dim + 4, ia4); -}
@@ -1,167 +0,0 @@
-#include "immintrin.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -void reim_invtwiddle_ifft_ref(uint64_t h, double* re, double* im, double om[2]); -void reim_invbitwiddle_ifft_ref(uint64_t h, double* re, double* im, double om[4]); - -__always_inline void reim_invtwiddle_ifft_avx2_fma(uint32_t h, double* re, double* im, double om[2]) { - const __m128d omx = _mm_load_pd(om); - const __m256d omra = _mm256_set_m128d(omx, omx); - const __m256d omi = _mm256_unpackhi_pd(omra, omra); - const __m256d omr = _mm256_unpacklo_pd(omra, omra); - double* r0 = re; - double* r1 = re + h; - double* i0 = im; - double* i1 = im + h; - for (uint32_t i = 0; i < h; i += 4) { - __m256d ur0 = _mm256_loadu_pd(r0 + i); - __m256d ur1 = _mm256_loadu_pd(r1 + i); - __m256d ui0 = _mm256_loadu_pd(i0 + i); - __m256d ui1 = _mm256_loadu_pd(i1 + i); - __m256d tra = _mm256_sub_pd(ur0, ur1); - __m256d tia = _mm256_sub_pd(ui0, ui1); - ur0 = _mm256_add_pd(ur0, ur1); - ui0 = _mm256_add_pd(ui0, ui1); - ur1 = _mm256_mul_pd(omi, tia); - ui1 = _mm256_mul_pd(omi, tra); - ur1 = _mm256_fmsub_pd(omr, tra, ur1); - ui1 = _mm256_fmadd_pd(omr, tia, ui1); - _mm256_storeu_pd(r0 + i, ur0); - _mm256_storeu_pd(r1 + i, ur1); - _mm256_storeu_pd(i0 + i, ui0); - _mm256_storeu_pd(i1 + i, ui1); - } -} - -__always_inline void reim_invbitwiddle_ifft_avx2_fma(uint32_t h, double* re, double* im, double om[4]) { - // reim_invbitwiddle_ifft_ref(h,re,im, om); - double* const r0 = re; - double* const r1 = re + h; - double* const r2 = re + 2 * h; - double* const r3 = re + 3 * h; - double* const i0 = im; - double* const i1 = im + h; - double* const i2 = im + 2 * h; - double* const i3 = im + 3 * h; - const __m256d om0 = _mm256_loadu_pd(om); - const __m256d omb = _mm256_permute2f128_pd(om0, om0, 0x11); - const __m256d oma = _mm256_permute2f128_pd(om0, om0, 0x00); - const __m256d omai = _mm256_unpackhi_pd(oma, oma); - const __m256d omar = _mm256_unpacklo_pd(oma, oma); - const __m256d ombi = _mm256_unpackhi_pd(omb, omb); - const __m256d ombr = _mm256_unpacklo_pd(omb, omb); - for (uint32_t i = 0; i < h; i += 4) { - __m256d ur0 = _mm256_loadu_pd(r0 + i); - __m256d ur1 = _mm256_loadu_pd(r1 + i); - __m256d ur2 = _mm256_loadu_pd(r2 + i); - __m256d ur3 = _mm256_loadu_pd(r3 + i); - __m256d ui0 = _mm256_loadu_pd(i0 + i); - __m256d ui1 = _mm256_loadu_pd(i1 + i); - __m256d ui2 = _mm256_loadu_pd(i2 + i); - __m256d ui3 = _mm256_loadu_pd(i3 + i); - __m256d tra, trb, tia, tib; - //------ twiddles 2 - tra = _mm256_sub_pd(ur0, ur1); - trb = _mm256_sub_pd(ur2, ur3); - tia = _mm256_sub_pd(ui0, ui1); - tib = _mm256_sub_pd(ui2, ui3); - ur0 = _mm256_add_pd(ur0, ur1); - ur2 = _mm256_add_pd(ur2, ur3); - ui0 = _mm256_add_pd(ui0, ui1); - ui2 = _mm256_add_pd(ui2, ui3); - ur1 = _mm256_mul_pd(omai, tia); - ur3 = _mm256_mul_pd(omar, tib); - ui1 = _mm256_mul_pd(omai, tra); - ui3 = _mm256_mul_pd(omar, trb); - ur1 = _mm256_fmsub_pd(omar, tra, ur1); - ur3 = _mm256_fmadd_pd(omai, trb, ur3); - ui1 = _mm256_fmadd_pd(omar, tia, ui1); - ui3 = _mm256_fmsub_pd(omai, tib, ui3); - //------ twiddles 1 - tra = _mm256_sub_pd(ur0, ur2); - trb = _mm256_sub_pd(ur1, ur3); - tia = _mm256_sub_pd(ui0, ui2); - tib = _mm256_sub_pd(ui1, ui3); - ur0 = _mm256_add_pd(ur0, ur2); - ur1 = _mm256_add_pd(ur1, ur3); - ui0 = _mm256_add_pd(ui0, ui2); - ui1 = _mm256_add_pd(ui1, ui3); - ur2 = _mm256_mul_pd(ombi, tia); - ur3 = _mm256_mul_pd(ombi, tib); - ui2 = _mm256_mul_pd(ombi, tra); - ui3 = _mm256_mul_pd(ombi, trb); - ur2 = _mm256_fmsub_pd(ombr, tra, ur2); - ur3 = _mm256_fmsub_pd(ombr, trb, ur3); - ui2 = _mm256_fmadd_pd(ombr, tia, ui2); - ui3 = _mm256_fmadd_pd(ombr, tib, ui3); - ///--- - _mm256_storeu_pd(r0 + i, ur0); - _mm256_storeu_pd(r1 + i, ur1); - _mm256_storeu_pd(r2 + i, ur2); - _mm256_storeu_pd(r3 + i, ur3); - _mm256_storeu_pd(i0 + i, ui0); - _mm256_storeu_pd(i1 + i, ui1); - _mm256_storeu_pd(i2 + i, ui2); - _mm256_storeu_pd(i3 + i, ui3); - } -} - -void reim_ifft_bfs_16_avx2_fma(uint32_t m, double* re, double* im, double** omg) { - uint32_t log2m = _mm_popcnt_u32(m - 1); // log2(m); - for (uint32_t off = 0; off < m; off += 16) { - reim_ifft16_avx_fma(re + off, im + off, *omg); - *omg += 16; - } - uint32_t h = 16; - uint32_t ms2 = m >> 1; // m/2 - while (h < ms2) { - uint32_t mm = h << 2; - for (uint32_t off = 0; off < m; off += mm) { - reim_invbitwiddle_ifft_avx2_fma(h, re + off, im + off, *omg); - *omg += 4; - } - h = mm; - } - if (log2m & 1) { - // if (h!=ms2) abort(); // bug - // do the first twiddle iteration normally - reim_invtwiddle_ifft_avx2_fma(h, re, im, *omg); - *omg += 2; - // h = m; - } -} - -void reim_ifft_rec_16_avx2_fma(uint32_t m, double* re, double* im, double** omg) { - if (m <= 2048) return reim_ifft_bfs_16_avx2_fma(m, re, im, omg); - const uint32_t h = m >> 1; // m / 2; - reim_ifft_rec_16_avx2_fma(h, re, im, omg); - reim_ifft_rec_16_avx2_fma(h, re + h, im + h, omg); - reim_invtwiddle_ifft_avx2_fma(h, re, im, *omg); - *omg += 2; -} - -void reim_ifft_avx2_fma(const REIM_IFFT_PRECOMP* precomp, double* dat) { - const int32_t m = precomp->m; - double* omg = precomp->powomegas; - double* re = dat; - double* im = dat + m; - if (m <= 16) { - switch (m) { - case 1: - return; - case 2: - return reim_ifft2_ref(re, im, omg); - case 4: - return reim_ifft4_avx_fma(re, im, omg); - case 8: - return reim_ifft8_avx_fma(re, im, omg); - case 16: - return reim_ifft16_avx_fma(re, im, omg); - default: - abort(); // m is not a power of 2 - } - } - if (m <= 2048) return reim_ifft_bfs_16_avx2_fma(m, re, im, &omg); - return reim_ifft_rec_16_avx2_fma(m, re, im, &omg); -}
@@ -1,408 +0,0 @@
-#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -void reim_invctwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim) { - double rdiff = *ra - *rb; - double idiff = *ia - *ib; - *ra = *ra + *rb; - *ia = *ia + *ib; - *rb = rdiff * omre - idiff * omim; - *ib = rdiff * omim + idiff * omre; -} - -// i (omre + i omim) = -omim + i omre -void reim_invcitwiddle(double* ra, double* ia, double* rb, double* ib, double omre, double omim) { - double rdiff = *ra - *rb; - double idiff = *ia - *ib; - *ra = *ra + *rb; - *ia = *ia + *ib; - *rb = rdiff * omim + idiff * omre; - *ib = -rdiff * omre + idiff * omim; -} - -void reim_ifft16_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omare = om[0]; - double ombre = om[1]; - double omcre = om[2]; - double omdre = om[3]; - double omaim = om[4]; - double ombim = om[5]; - double omcim = om[6]; - double omdim = om[7]; - reim_invctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - reim_invcitwiddle(&dre[2], &dim[2], &dre[3], &dim[3], omare, omaim); - reim_invctwiddle(&dre[4], &dim[4], &dre[5], &dim[5], ombre, ombim); - reim_invcitwiddle(&dre[6], &dim[6], &dre[7], &dim[7], ombre, ombim); - reim_invctwiddle(&dre[8], &dim[8], &dre[9], &dim[9], omcre, omcim); - reim_invcitwiddle(&dre[10], &dim[10], &dre[11], &dim[11], omcre, omcim); - reim_invctwiddle(&dre[12], &dim[12], &dre[13], &dim[13], omdre, omdim); - reim_invcitwiddle(&dre[14], &dim[14], &dre[15], &dim[15], omdre, omdim); - } - { - double omare = om[8]; - double omaim = om[9]; - double ombre = om[10]; - double ombim = om[11]; - reim_invctwiddle(&dre[0], &dim[0], &dre[2], &dim[2], omare, omaim); - reim_invctwiddle(&dre[1], &dim[1], &dre[3], &dim[3], omare, omaim); - reim_invcitwiddle(&dre[4], &dim[4], &dre[6], &dim[6], omare, omaim); - reim_invcitwiddle(&dre[5], &dim[5], &dre[7], &dim[7], omare, omaim); - reim_invctwiddle(&dre[8], &dim[8], &dre[10], &dim[10], ombre, ombim); - reim_invctwiddle(&dre[9], &dim[9], &dre[11], &dim[11], ombre, ombim); - reim_invcitwiddle(&dre[12], &dim[12], &dre[14], &dim[14], ombre, ombim); - reim_invcitwiddle(&dre[13], &dim[13], &dre[15], &dim[15], ombre, ombim); - } - { - double omre = om[12]; - double omim = om[13]; - reim_invctwiddle(&dre[0], &dim[0], &dre[4], &dim[4], omre, omim); - reim_invctwiddle(&dre[1], &dim[1], &dre[5], &dim[5], omre, omim); - reim_invctwiddle(&dre[2], &dim[2], &dre[6], &dim[6], omre, omim); - reim_invctwiddle(&dre[3], &dim[3], &dre[7], &dim[7], omre, omim); - reim_invcitwiddle(&dre[8], &dim[8], &dre[12], &dim[12], omre, omim); - reim_invcitwiddle(&dre[9], &dim[9], &dre[13], &dim[13], omre, omim); - reim_invcitwiddle(&dre[10], &dim[10], &dre[14], &dim[14], omre, omim); - reim_invcitwiddle(&dre[11], &dim[11], &dre[15], &dim[15], omre, omim); - } - { - double omre = om[14]; - double omim = om[15]; - reim_invctwiddle(&dre[0], &dim[0], &dre[8], &dim[8], omre, omim); - reim_invctwiddle(&dre[1], &dim[1], &dre[9], &dim[9], omre, omim); - reim_invctwiddle(&dre[2], &dim[2], &dre[10], &dim[10], omre, omim); - reim_invctwiddle(&dre[3], &dim[3], &dre[11], &dim[11], omre, omim); - reim_invctwiddle(&dre[4], &dim[4], &dre[12], &dim[12], omre, omim); - reim_invctwiddle(&dre[5], &dim[5], &dre[13], &dim[13], omre, omim); - reim_invctwiddle(&dre[6], &dim[6], &dre[14], &dim[14], omre, omim); - reim_invctwiddle(&dre[7], &dim[7], &dre[15], &dim[15], omre, omim); - } -} - -void fill_reim_ifft16_omegas(const double entry_pwr, double** omg) { - const double j_pow = 1. / 8.; - const double k_pow = 1. / 16.; - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - const double pin_4 = entry_pwr / 8.; - const double pin_8 = entry_pwr / 16.; - // ((8,9,10,11),(12,13,14,15)) are 4 reals then 4 imag of om^1/8*(1,k,j,kj) - (*omg)[0] = cos(2. * M_PI * (pin_8)); - (*omg)[1] = cos(2. * M_PI * (pin_8 + j_pow)); - (*omg)[2] = cos(2. * M_PI * (pin_8 + k_pow)); - (*omg)[3] = cos(2. * M_PI * (pin_8 + j_pow + k_pow)); - (*omg)[4] = -sin(2. * M_PI * (pin_8)); - (*omg)[5] = -sin(2. * M_PI * (pin_8 + j_pow)); - (*omg)[6] = -sin(2. * M_PI * (pin_8 + k_pow)); - (*omg)[7] = -sin(2. * M_PI * (pin_8 + j_pow + k_pow)); - // (4,5) and (6,7) are real and imag of om^1/4 and j.om^1/4 - (*omg)[8] = cos(2. * M_PI * (pin_4)); - (*omg)[9] = -sin(2. * M_PI * (pin_4)); - (*omg)[10] = cos(2. * M_PI * (pin_4 + j_pow)); - (*omg)[11] = -sin(2. * M_PI * (pin_4 + j_pow)); - // 2 and 3 are real and imag of om^1/2 - (*omg)[12] = cos(2. * M_PI * (pin_2)); - (*omg)[13] = -sin(2. * M_PI * (pin_2)); - // 0 and 1 are real and imag of om - (*omg)[14] = cos(2. * M_PI * pin); - (*omg)[15] = -sin(2. * M_PI * pin); - *omg += 16; -} - -void reim_ifft8_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omare = om[0]; - double ombre = om[1]; - double omaim = om[2]; - double ombim = om[3]; - reim_invctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - reim_invcitwiddle(&dre[2], &dim[2], &dre[3], &dim[3], omare, omaim); - reim_invctwiddle(&dre[4], &dim[4], &dre[5], &dim[5], ombre, ombim); - reim_invcitwiddle(&dre[6], &dim[6], &dre[7], &dim[7], ombre, ombim); - } - { - double omare = om[4]; - double omaim = om[5]; - reim_invctwiddle(&dre[0], &dim[0], &dre[2], &dim[2], omare, omaim); - reim_invctwiddle(&dre[1], &dim[1], &dre[3], &dim[3], omare, omaim); - reim_invcitwiddle(&dre[4], &dim[4], &dre[6], &dim[6], omare, omaim); - reim_invcitwiddle(&dre[5], &dim[5], &dre[7], &dim[7], omare, omaim); - } - { - double omre = om[6]; - double omim = om[7]; - reim_invctwiddle(&dre[0], &dim[0], &dre[4], &dim[4], omre, omim); - reim_invctwiddle(&dre[1], &dim[1], &dre[5], &dim[5], omre, omim); - reim_invctwiddle(&dre[2], &dim[2], &dre[6], &dim[6], omre, omim); - reim_invctwiddle(&dre[3], &dim[3], &dre[7], &dim[7], omre, omim); - } -} - -void fill_reim_ifft8_omegas(const double entry_pwr, double** omg) { - const double j_pow = 1. / 8.; - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - const double pin_4 = entry_pwr / 8.; - // (4,5) and (6,7) are real and imag of om^1/4 and j.om^1/4 - (*omg)[0] = cos(2. * M_PI * (pin_4)); - (*omg)[1] = cos(2. * M_PI * (pin_4 + j_pow)); - (*omg)[2] = -sin(2. * M_PI * (pin_4)); - (*omg)[3] = -sin(2. * M_PI * (pin_4 + j_pow)); - // 2 and 3 are real and imag of om^1/2 - (*omg)[4] = cos(2. * M_PI * (pin_2)); - (*omg)[5] = -sin(2. * M_PI * (pin_2)); - // 0 and 1 are real and imag of om - (*omg)[6] = cos(2. * M_PI * pin); - (*omg)[7] = -sin(2. * M_PI * pin); - *omg += 8; -} - -void reim_ifft4_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omare = om[0]; - double omaim = om[1]; - reim_invctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - reim_invcitwiddle(&dre[2], &dim[2], &dre[3], &dim[3], omare, omaim); - } - { - double omare = om[2]; - double omaim = om[3]; - reim_invctwiddle(&dre[0], &dim[0], &dre[2], &dim[2], omare, omaim); - reim_invctwiddle(&dre[1], &dim[1], &dre[3], &dim[3], omare, omaim); - } -} - -void fill_reim_ifft4_omegas(const double entry_pwr, double** omg) { - const double pin = entry_pwr / 2.; - const double pin_2 = entry_pwr / 4.; - // 2 and 3 are real and imag of om^1/2 - (*omg)[0] = cos(2. * M_PI * (pin_2)); - (*omg)[1] = -sin(2. * M_PI * (pin_2)); - // 0 and 1 are real and imag of om - (*omg)[2] = cos(2. * M_PI * pin); - (*omg)[3] = -sin(2. * M_PI * pin); - *omg += 4; -} - -void reim_ifft2_ref(double* dre, double* dim, const void* pom) { - const double* om = (const double*)pom; - { - double omare = om[0]; - double omaim = om[1]; - reim_invctwiddle(&dre[0], &dim[0], &dre[1], &dim[1], omare, omaim); - } -} - -void fill_reim_ifft2_omegas(const double entry_pwr, double** omg) { - const double pin = entry_pwr / 2.; - // 0 and 1 are real and imag of om - (*omg)[0] = cos(2. * M_PI * pin); - (*omg)[1] = -sin(2. * M_PI * pin); - *omg += 2; -} - -void reim_invtwiddle_ifft_ref(uint64_t h, double* re, double* im, double om[2]) { - for (uint64_t i = 0; i < h; ++i) { - reim_invctwiddle(&re[i], &im[i], &re[h + i], &im[h + i], om[0], om[1]); - } -} - -void reim_invbitwiddle_ifft_ref(uint64_t h, double* re, double* im, double om[4]) { - double* r0 = re; - double* r1 = re + h; - double* r2 = re + 2 * h; - double* r3 = re + 3 * h; - double* i0 = im; - double* i1 = im + h; - double* i2 = im + 2 * h; - double* i3 = im + 3 * h; - for (uint64_t i = 0; i < h; ++i) { - reim_invctwiddle(&r0[i], &i0[i], &r1[i], &i1[i], om[0], om[1]); - reim_invcitwiddle(&r2[i], &i2[i], &r3[i], &i3[i], om[0], om[1]); - } - for (uint64_t i = 0; i < h; ++i) { - reim_invctwiddle(&r0[i], &i0[i], &r2[i], &i2[i], om[2], om[3]); - reim_invctwiddle(&r1[i], &i1[i], &r3[i], &i3[i], om[2], om[3]); - } -} - -void reim_ifft_bfs_16_ref(uint64_t m, double* re, double* im, double** omg) { - uint64_t log2m = log2(m); - for (uint64_t off = 0; off < m; off += 16) { - reim_ifft16_ref(re + off, im + off, *omg); - *omg += 16; - } - uint64_t h = 16; - uint64_t ms2 = m / 2; - while (h < ms2) { - uint64_t mm = h << 2; - for (uint64_t off = 0; off < m; off += mm) { - reim_invbitwiddle_ifft_ref(h, re + off, im + off, *omg); - *omg += 4; - } - h = mm; - } - if (log2m % 2 != 0) { - if (h != ms2) abort(); // bug - // do the first twiddle iteration normally - reim_invtwiddle_ifft_ref(h, re, im, *omg); - *omg += 2; - h = m; - } -} - -void fill_reim_ifft_bfs_16_omegas(uint64_t m, double entry_pwr, double** omg) { - uint64_t log2m = log2(m); - // uint64_t mm = 16; - double ss = entry_pwr * 16. / m; - for (uint64_t off = 0; off < m; off += 16) { - double s = ss + fracrevbits(off / 16); - fill_reim_ifft16_omegas(s, omg); - } - uint64_t h = 16; - uint64_t ms2 = m / 2; - while (h < ms2) { - uint64_t mm = h << 2; - for (uint64_t off = 0; off < m; off += mm) { - double rs0 = ss + fracrevbits(off / mm) / 4.; - double rs1 = 2. * rs0; - (*omg)[0] = cos(2 * M_PI * rs0); - (*omg)[1] = -sin(2 * M_PI * rs0); - (*omg)[2] = cos(2 * M_PI * rs1); - (*omg)[3] = -sin(2 * M_PI * rs1); - *omg += 4; - } - ss *= 4.; - h = mm; - } - if (log2m % 2 != 0) { - if (h != ms2) abort(); // bug - // do the first twiddle iteration normally - (*omg)[0] = cos(2 * M_PI * ss); - (*omg)[1] = -sin(2 * M_PI * ss); - *omg += 2; - h = m; - ss *= 2.; - } - if (ss != entry_pwr) abort(); -} - -void reim_ifft_rec_16_ref(uint64_t m, double* re, double* im, double** omg) { - if (m <= 2048) return reim_ifft_bfs_16_ref(m, re, im, omg); - const uint32_t h = m / 2; - reim_ifft_rec_16_ref(h, re, im, omg); - reim_ifft_rec_16_ref(h, re + h, im + h, omg); - reim_invtwiddle_ifft_ref(h, re, im, *omg); - *omg += 2; -} - -void fill_reim_ifft_rec_16_omegas(uint64_t m, double entry_pwr, double** omg) { - if (m <= 2048) return fill_reim_ifft_bfs_16_omegas(m, entry_pwr, omg); - const uint64_t h = m / 2; - const double s = entry_pwr / 2; - fill_reim_ifft_rec_16_omegas(h, s, omg); - fill_reim_ifft_rec_16_omegas(h, s + 0.5, omg); - (*omg)[0] = cos(2 * M_PI * s); - (*omg)[1] = -sin(2 * M_PI * s); - *omg += 2; -} - -void reim_ifft_ref(const REIM_IFFT_PRECOMP* precomp, double* dat) { - const int32_t m = precomp->m; - double* omg = precomp->powomegas; - double* re = dat; - double* im = dat + m; - if (m <= 16) { - switch (m) { - case 1: - return; - case 2: - return reim_ifft2_ref(re, im, omg); - case 4: - return reim_ifft4_ref(re, im, omg); - case 8: - return reim_ifft8_ref(re, im, omg); - case 16: - return reim_ifft16_ref(re, im, omg); - default: - abort(); // m is not a power of 2 - } - } - if (m <= 2048) return reim_ifft_bfs_16_ref(m, re, im, &omg); - return reim_ifft_rec_16_ref(m, re, im, &omg); -} - -EXPORT REIM_IFFT_PRECOMP* new_reim_ifft_precomp(uint32_t m, uint32_t num_buffers) { - const uint64_t OMG_SPACE = ceilto64b(2 * m * sizeof(double)); - const uint64_t BUF_SIZE = ceilto64b(2 * m * sizeof(double)); - void* reps = malloc(sizeof(REIM_IFFT_PRECOMP) // base - + 63 // padding - + OMG_SPACE // tables //TODO 16? - + num_buffers * BUF_SIZE // buffers - ); - uint64_t aligned_addr = ceilto64b((uint64_t)(reps) + sizeof(REIM_IFFT_PRECOMP)); - REIM_IFFT_PRECOMP* r = (REIM_IFFT_PRECOMP*)reps; - r->m = m; - r->buf_size = BUF_SIZE; - r->powomegas = (double*)aligned_addr; - r->aligned_buffers = (void*)(aligned_addr + OMG_SPACE); - // fill in powomegas - double* omg = (double*)r->powomegas; - if (m <= 16) { - switch (m) { - case 1: - break; - case 2: - fill_reim_ifft2_omegas(0.25, &omg); - break; - case 4: - fill_reim_ifft4_omegas(0.25, &omg); - break; - case 8: - fill_reim_ifft8_omegas(0.25, &omg); - break; - case 16: - fill_reim_ifft16_omegas(0.25, &omg); - break; - default: - abort(); // m is not a power of 2 - } - } else if (m <= 2048) { - fill_reim_ifft_bfs_16_omegas(m, 0.25, &omg); - } else { - fill_reim_ifft_rec_16_omegas(m, 0.25, &omg); - } - if (((uint64_t)omg) - aligned_addr > OMG_SPACE) abort(); - // dispatch the right implementation - { - if (CPU_SUPPORTS("fma")) { - r->function = reim_ifft_avx2_fma; - } else { - r->function = reim_ifft_ref; - } - } - return reps; -} - -void reim_naive_ifft(uint64_t m, double entry_pwr, double* re, double* im) { - if (m == 1) return; - // twiddle - const uint64_t h = m / 2; - const double s = entry_pwr / 2.; - reim_naive_ifft(h, s, re, im); - reim_naive_ifft(h, s + 0.5, re + h, im + h); - const double sre = cos(2 * M_PI * s); - const double sim = -sin(2 * M_PI * s); - for (uint64_t j = 0; j < h; ++j) { - double rdiff = re[j] - re[h + j]; - double idiff = im[j] - im[h + j]; - re[j] = re[j] + re[h + j]; - im[j] = im[j] + im[h + j]; - re[h + j] = rdiff * sre - idiff * sim; - im[h + j] = idiff * sre + rdiff * sim; - } -}
@@ -1,35 +0,0 @@
-#include <errno.h> -#include <immintrin.h> -#include <string.h> - -#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -typedef union { - double d; - uint64_t u; -} dblui64_t; - -EXPORT void reim_to_tnx_avx(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* x) { - const uint64_t n = tables->m << 1; - const __m256d add_cst = _mm256_set1_pd(tables->add_cst); - const __m256d mask_and = _mm256_castsi256_pd(_mm256_set1_epi64x(tables->mask_and)); - const __m256d mask_or = _mm256_castsi256_pd(_mm256_set1_epi64x(tables->mask_or)); - const __m256d sub_cst = _mm256_set1_pd(tables->sub_cst); - __m256d reg0, reg1; - for (uint64_t i = 0; i < n; i += 8) { - reg0 = _mm256_loadu_pd(x + i); - reg1 = _mm256_loadu_pd(x + i + 4); - reg0 = _mm256_add_pd(reg0, add_cst); - reg1 = _mm256_add_pd(reg1, add_cst); - reg0 = _mm256_and_pd(reg0, mask_and); - reg1 = _mm256_and_pd(reg1, mask_and); - reg0 = _mm256_or_pd(reg0, mask_or); - reg1 = _mm256_or_pd(reg1, mask_or); - reg0 = _mm256_sub_pd(reg0, sub_cst); - reg1 = _mm256_sub_pd(reg1, sub_cst); - _mm256_storeu_pd(r + i, reg0); - _mm256_storeu_pd(r + i + 4, reg1); - } -}
@@ -1,75 +0,0 @@
-#include <errno.h> -#include <string.h> - -#include "../commons_private.h" -#include "reim_fft_internal.h" -#include "reim_fft_private.h" - -EXPORT void reim_to_tnx_basic_ref(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* x) { - const uint64_t n = tables->m << 1; - double divisor = tables->divisor; - for (uint64_t i = 0; i < n; ++i) { - double ri = x[i] / divisor; - r[i] = ri - rint(ri); - } -} - -typedef union { - double d; - uint64_t u; -} dblui64_t; - -EXPORT void reim_to_tnx_ref(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* x) { - const uint64_t n = tables->m << 1; - double add_cst = tables->add_cst; - uint64_t mask_and = tables->mask_and; - uint64_t mask_or = tables->mask_or; - double sub_cst = tables->sub_cst; - dblui64_t cur; - for (uint64_t i = 0; i < n; ++i) { - cur.d = x[i] + add_cst; - cur.u &= mask_and; - cur.u |= mask_or; - r[i] = cur.d - sub_cst; - } -} - -void* init_reim_to_tnx_precomp(REIM_TO_TNX_PRECOMP* const res, uint32_t m, double divisor, uint32_t log2overhead) { - if (m & (m - 1)) return spqlios_error("m must be a power of 2"); - if (is_not_pow2_double(&divisor)) return spqlios_error("divisor must be a power of 2"); - if (log2overhead > 52) return spqlios_error("log2overhead is too large"); - res->m = m; - res->divisor = divisor; - res->log2overhead = log2overhead; - // 52 + 11 + 1 - // ......1.......01(1)|expo|sign - // .......=========(1)|expo|sign msbbits = log2ovh + 2 + 11 + 1 - uint64_t nbits = 50 - log2overhead; - dblui64_t ovh_cst; - ovh_cst.d = 0.5 + (6 << log2overhead); - res->add_cst = ovh_cst.d * divisor; - res->mask_and = ((UINT64_C(1) << nbits) - 1); - res->mask_or = ovh_cst.u & ((UINT64_C(-1)) << nbits); - res->sub_cst = ovh_cst.d; - // TODO: check selection logic - if (CPU_SUPPORTS("avx2")) { - if (m >= 8) { - res->function = reim_to_tnx_avx; - } else { - res->function = reim_to_tnx_ref; - } - } else { - res->function = reim_to_tnx_ref; - } - return res; -} - -EXPORT REIM_TO_TNX_PRECOMP* new_reim_to_tnx_precomp(uint32_t m, double divisor, uint32_t log2overhead) { - REIM_TO_TNX_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim_to_tnx_precomp(res, m, divisor, log2overhead)); -} - -EXPORT void reim_to_tnx(const REIM_TO_TNX_PRECOMP* tables, double* r, const double* x) { - tables->function(tables, r, x); -}
@@ -1,177 +0,0 @@
-#ifndef SPQLIOS_REIM4_ARITHMETIC_H -#define SPQLIOS_REIM4_ARITHMETIC_H - -#include "../commons.h" - -// the reim4 structure represent 4 complex numbers, -// represented as re0,re1,re2,re3,im0,im1,im2,im3 - -// TODO implement these basic arithmetic functions. (only ref is needed) - -/** @brief dest = 0 */ -EXPORT void reim4_zero(double* dest); -/** @brief dest = a + b */ -EXPORT void reim4_add(double* dest, const double* u, const double* v); -/** @brief dest = a * b */ -EXPORT void reim4_mul(double* dest, const double* u, const double* v); -/** @brief dest += a * b */ -EXPORT void reim4_add_mul(double* dest, const double* a, const double* b); - -// TODO add the dot products: vec x mat1col, vec x mat2cols functions here (ref and avx) - -// TODO implement the convolution functions here (ref and avx) -/** - * @brief k-th coefficient of the convolution product a * b. - * - * The k-th coefficient is defined as sum_{i+j=k} a[i].b[j]. - * (in this sum, i and j must remain within the bounds [0,sizea[ and [0,sizeb[) - * - * In practice, accounting for these bounds, the convolution function boils down to - * ``` - * res := 0 - * if (k<sizeb+sizea-1) then - * for j from max(0,k+1-sizea) incl to min(sizeb,k+1) excl do - * res += a[k-j] * b[j] - * return res - * ``` - */ -EXPORT void reim4_convolution_1coeff_ref(uint64_t k, double* dest, const double* a, uint64_t sizea, const double* b, - uint64_t sizeb); -EXPORT void reim4_convolution_1coeff_avx(uint64_t k, double* dest, const double* a, uint64_t sizea, const double* b, - uint64_t sizeb); -/** @brief returns two consecutive convolution coefficients: k and k+1 */ -EXPORT void reim4_convolution_2coeff_ref(uint64_t k, double* dest, const double* a, uint64_t sizea, const double* b, - uint64_t sizeb); -EXPORT void reim4_convolution_2coeff_avx(uint64_t k, double* dest, const double* a, uint64_t sizea, const double* b, - uint64_t sizeb); - -/** - * @brief From the convolution a * b, return the coefficients between offset and offset + size - * For the full convolution, use offset=0 and size=sizea+sizeb-1. - */ -EXPORT void reim4_convolution_ref(double* dest, uint64_t dest_size, uint64_t dest_offset, const double* a, - uint64_t sizea, const double* b, uint64_t sizeb); -EXPORT void reim4_convolution_avx(double* dest, uint64_t dest_size, uint64_t dest_offset, const double* a, - uint64_t sizea, const double* b, uint64_t sizeb); - -// The reim fft layout that encodes m = nn/2 complex evaluations -// can be decomposed into m/4 reim4 blocks -// the i-th block of v is noted v(i); -// it contains the 4 evaluations (e[4i],e[4i+1],e[4i+2],e[4i+3]) - -/** - * @brief extract 1 reim4 block from one reim vectors of m complexes - * @param m the size of each reim - * @param blk the block id to extract (<m/4) - * @param dst the output: nrows reim4's dst[i] = src[i](blk) - * @param src the input: nrows reim's - */ -EXPORT void reim4_extract_1blk_from_reim_ref(uint64_t m, uint64_t blk, - double* const dst, // 8 doubles - const double* const src // a reim vector -); -EXPORT void reim4_extract_1blk_from_reim_avx(uint64_t m, uint64_t blk, - double* const dst, // 8 doubles - const double* const src // a reim vectors -); - -/** - * @brief extract 1 reim vectors from one reim4 block of m complexes - * @param m the size of each reim - * @param blk the block id to extract (<m/4) - * @param dst the output: nrows reim4's dst[i](blk) = src[i] - * @param src the input: nrows reim's - */ -EXPORT void reim4_extract_reim_from_1blk_ref(uint64_t m, uint64_t blk, - double* const dst, // 8 doubles - const double* const src // a reim vector -); - -/** - * @brief extract 1 reim4 block from nrows reim vectors of m complexes - * @param nrows the number of reim (fft) vectors - * @param m the size of each reim - * @param blk the block id to extract (<m/4) - * @param dst the output: nrows reim4's dst[i] = src[i](blk) - * @param src the input: nrows reim's - */ -EXPORT void reim4_extract_1blk_from_contiguous_reim_ref(uint64_t m, uint64_t nrows, uint64_t blk, double* const dst, - const double* const src); -EXPORT void reim4_extract_1blk_from_contiguous_reim_avx(uint64_t m, uint64_t nrows, uint64_t blk, double* const dst, - const double* const src); - -/** - * @brief extract 1 reim4 block from nrows reim vectors of m complexes with slice sl - * @param m the size of each reim - * @param sl the slice size - * @param nrows the number of reim (fft) vectors to extract - * @param blk the block id to extract (<m/4) - * @param dst the output: nrows reim4's dst[i] = src[i](blk) - * @param src the input: nrows reim's - */ -EXPORT void reim4_extract_1blk_from_contiguous_reim_sl_ref(uint64_t m, uint64_t sl, uint64_t nrows, uint64_t blk, - double* const dst, const double* const src); -EXPORT void reim4_extract_1blk_from_contiguous_reim_sl_avx(uint64_t m, uint64_t sl, uint64_t nrows, uint64_t blk, - double* const dst, const double* const src); - -/** - * @brief saves 1 single reim4 block in a reim vectors of m complexes - * @param m the size of each reim - * @param blk the block id to save (<m/4) - * @param dest the output reim: dst(blk) = src - * @param src the input reim4 - */ -EXPORT void reim4_save_1blk_to_reim_ref(uint64_t m, uint64_t blk, - double* dest, // 1 reim vector of length m - const double* src // 8 doubles -); -EXPORT void reim4_save_1blk_to_reim_avx(uint64_t m, uint64_t blk, - double* dest, // 1 reim vector of length m - const double* src // 8 doubles -); - -/** - * @brief Adds 1 single reim4 block in a reim vectors of m complexes - * @param m the size of each reim - * @param blk the block id to save (<m/4) - * @param dest the output reim: dst(blk) += src - * @param src the input reim4 - */ -EXPORT void reim4_add_1blk_to_reim_ref(uint64_t m, uint64_t blk, - double* dest, // 1 reim vector of length m - const double* src // 8 doubles -); -EXPORT void reim4_add_1blk_to_reim_avx(uint64_t m, uint64_t blk, - double* dest, // 1 reim vector of length m - const double* src // 8 doubles -); - -// dest = sum u[i].v[i] -// make ref and avx2 implem, needs to be fast -// can accumulate the real and imaginary parts on 2 separate registers, and combine at the end -EXPORT void reim4_vec_mat1col_product_ref(const uint64_t nrows, - double* const dst, // 8 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 8 doubles -); - -EXPORT void reim4_vec_mat1col_product_avx2(const uint64_t nrows, - double* const dst, // 8 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 8 doubles -); - -// dest = sum u[i].v[i] -// make ref and avx2 implem, needs to be fast -// can accumulate the real and imaginary parts on 2 separate registers, and recombine at the end -EXPORT void reim4_vec_mat2cols_product_ref(uint64_t nrows, double* const dst, // 16 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 16 doubles -); - -EXPORT void reim4_vec_mat2cols_product_avx2(uint64_t nrows, double* const dst, // 16 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 16 doubles -); - -#endif // SPQLIOS_REIM4_ARITHMETIC_H
@@ -1,147 +0,0 @@
-#include <assert.h> -#include <immintrin.h> - -#include "reim4_arithmetic.h" - -void reim4_extract_1blk_from_reim_avx(uint64_t m, uint64_t blk, - double* const dst, // nrows * 8 doubles - const double* const src // a contiguous array of nrows reim vectors -) { - assert(blk < (m >> 2)); - const double* src_ptr = src + (blk << 2); - double* dst_ptr = dst; - _mm256_storeu_pd(dst_ptr, _mm256_loadu_pd(src_ptr)); - _mm256_storeu_pd(dst_ptr + 4, _mm256_loadu_pd(src_ptr + m)); -} - -void reim4_extract_1blk_from_contiguous_reim_avx(uint64_t m, uint64_t nrows, uint64_t blk, double* const dst, - const double* const src) { - assert(blk < (m >> 2)); - const double* src_ptr = src + (blk << 2); - double* dst_ptr = dst; - for (uint64_t i = 0; i < nrows * 2; ++i) { - _mm256_storeu_pd(dst_ptr, _mm256_loadu_pd(src_ptr)); - dst_ptr += 4; - src_ptr += m; - } -} - -EXPORT void reim4_extract_1blk_from_contiguous_reim_sl_avx(uint64_t m, uint64_t sl, uint64_t nrows, uint64_t blk, - double* const dst, const double* const src) { - assert(blk < (m >> 2)); - const double* src_ptr = src + (blk << 2); - double* dst_ptr = dst; - for (uint64_t i = 0; i < nrows; ++i) { - _mm256_storeu_pd(dst_ptr, _mm256_loadu_pd(src_ptr)); - _mm256_storeu_pd(dst_ptr + 4, _mm256_loadu_pd(src_ptr + m)); - dst_ptr += 8; - src_ptr += sl; - } -} - -void reim4_save_1blk_to_reim_avx(uint64_t m, uint64_t blk, - double* dst, // 1 reim vector of length m - const double* src // 8 doubles -) { - assert(blk < (m >> 2)); - const double* src_ptr = src; - double* dst_ptr = dst + (blk << 2); - _mm256_storeu_pd(dst_ptr, _mm256_loadu_pd(src_ptr)); - _mm256_storeu_pd(dst_ptr + m, _mm256_loadu_pd(src_ptr + 4)); -} - -void reim4_add_1blk_to_reim_avx(uint64_t m, uint64_t blk, - double* dst, // 1 reim vector of length m - const double* src // 8 doubles -) { - assert(blk < (m >> 2)); - const double* src_ptr = src; - double* dst_ptr = dst + (blk << 2); - - __m256d s0 = _mm256_loadu_pd(src_ptr); - __m256d d0 = _mm256_loadu_pd(dst_ptr); - _mm256_storeu_pd(dst_ptr, _mm256_add_pd(s0, d0)); - - __m256d s1 = _mm256_loadu_pd(src_ptr + 4); - __m256d d1 = _mm256_loadu_pd(dst_ptr + m); - _mm256_storeu_pd(dst_ptr + m, _mm256_add_pd(s1, d1)); -} - -__always_inline void cplx_prod(__m256d* re1, __m256d* re2, __m256d* im, const double* const u_ptr, - const double* const v_ptr) { - const __m256d a = _mm256_loadu_pd(u_ptr); - const __m256d c = _mm256_loadu_pd(v_ptr); - *re1 = _mm256_fmadd_pd(a, c, *re1); - - const __m256d b = _mm256_loadu_pd(u_ptr + 4); - *im = _mm256_fmadd_pd(b, c, *im); - - const __m256d d = _mm256_loadu_pd(v_ptr + 4); - *re2 = _mm256_fmadd_pd(b, d, *re2); - *im = _mm256_fmadd_pd(a, d, *im); -} - -void reim4_vec_mat1col_product_avx2(const uint64_t nrows, - double* const dst, // 8 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 8 doubles -) { - __m256d re1 = _mm256_setzero_pd(); - __m256d re2 = _mm256_setzero_pd(); - __m256d im1 = _mm256_setzero_pd(); - __m256d im2 = _mm256_setzero_pd(); - - const double* u_ptr = u; - const double* v_ptr = v; - for (uint64_t i = 0; i < nrows; ++i) { - const __m256d a = _mm256_loadu_pd(u_ptr); - const __m256d c = _mm256_loadu_pd(v_ptr); - re1 = _mm256_fmadd_pd(a, c, re1); - - const __m256d b = _mm256_loadu_pd(u_ptr + 4); - im2 = _mm256_fmadd_pd(b, c, im2); - - const __m256d d = _mm256_loadu_pd(v_ptr + 4); - re2 = _mm256_fmadd_pd(b, d, re2); - im1 = _mm256_fmadd_pd(a, d, im1); - - u_ptr += 8; - v_ptr += 8; - } - - _mm256_storeu_pd(dst, _mm256_sub_pd(re1, re2)); - _mm256_storeu_pd(dst + 4, _mm256_add_pd(im1, im2)); -} - -EXPORT void reim4_vec_mat2cols_product_avx2(const uint64_t nrows, - double* const dst, // 16 doubles - const double* const u, // nrows * 16 doubles - const double* const v // nrows * 16 doubles -) { - __m256d re1 = _mm256_setzero_pd(); - __m256d im1 = _mm256_setzero_pd(); - __m256d re2 = _mm256_setzero_pd(); - __m256d im2 = _mm256_setzero_pd(); - - __m256d ur, ui, ar, ai, br, bi; - for (uint64_t i = 0; i < nrows; ++i) { - ur = _mm256_loadu_pd(u + 8 * i); - ui = _mm256_loadu_pd(u + 8 * i + 4); - ar = _mm256_loadu_pd(v + 16 * i); - ai = _mm256_loadu_pd(v + 16 * i + 4); - br = _mm256_loadu_pd(v + 16 * i + 8); - bi = _mm256_loadu_pd(v + 16 * i + 12); - re1 = _mm256_fmsub_pd(ui, ai, re1); - re2 = _mm256_fmsub_pd(ui, bi, re2); - im1 = _mm256_fmadd_pd(ur, ai, im1); - im2 = _mm256_fmadd_pd(ur, bi, im2); - re1 = _mm256_fmsub_pd(ur, ar, re1); - re2 = _mm256_fmsub_pd(ur, br, re2); - im1 = _mm256_fmadd_pd(ui, ar, im1); - im2 = _mm256_fmadd_pd(ui, br, im2); - } - _mm256_storeu_pd(dst, re1); - _mm256_storeu_pd(dst + 4, im1); - _mm256_storeu_pd(dst + 8, re2); - _mm256_storeu_pd(dst + 12, im2); -}
@@ -1,254 +0,0 @@
-#include <assert.h> -#include <string.h> - -#include "reim4_arithmetic.h" - -// Stores the first 4 values (RE) of src + blk*4 and he first 4 values (IM) of src + blk*4 + m -// contiguously into dst -void reim4_extract_1blk_from_reim_ref(uint64_t m, uint64_t blk, - double* const dst, // 8 doubles - const double* const src // one reim4 vector -) { - assert(blk < (m >> 2)); - const double* src_ptr = src + (blk << 2); - // copy the real parts - dst[0] = src_ptr[0]; - dst[1] = src_ptr[1]; - dst[2] = src_ptr[2]; - dst[3] = src_ptr[3]; - src_ptr += m; - // copy the imaginary parts - dst[4] = src_ptr[0]; - dst[5] = src_ptr[1]; - dst[6] = src_ptr[2]; - dst[7] = src_ptr[3]; -} - -void reim4_extract_reim_from_1blk_ref(uint64_t m, uint64_t blk, - double* const dst, // 8 doubles - const double* const src // one reim4 vector -) { - assert(blk < (m >> 2)); - double* dst_ptr = dst + (blk << 2); - // copy the real parts - dst_ptr[0] = src[0]; - dst_ptr[1] = src[1]; - dst_ptr[2] = src[2]; - dst_ptr[3] = src[3]; - dst_ptr += m; - // copy the imaginary parts - dst_ptr[0] = src[4]; - dst_ptr[1] = src[5]; - dst_ptr[2] = src[6]; - dst_ptr[3] = src[7]; -} - -EXPORT void reim4_extract_1blk_from_contiguous_reim_ref(uint64_t m, uint64_t nrows, uint64_t blk, double* const dst, - const double* const src) { - assert(blk < (m >> 2)); - - const double* src_ptr = src + (blk << 2); - double* dst_ptr = dst; - for (uint64_t i = 0; i < nrows * 2; ++i) { - dst_ptr[0] = src_ptr[0]; - dst_ptr[1] = src_ptr[1]; - dst_ptr[2] = src_ptr[2]; - dst_ptr[3] = src_ptr[3]; - dst_ptr += 4; - src_ptr += m; - } -} - -EXPORT void reim4_extract_1blk_from_contiguous_reim_sl_ref(uint64_t m, uint64_t sl, uint64_t nrows, uint64_t blk, - double* const dst, const double* const src) { - assert(blk < (m >> 2)); - - const double* src_ptr = src + (blk << 2); - double* dst_ptr = dst; - const uint64_t sl_minus_m = sl - m; - for (uint64_t i = 0; i < nrows; ++i) { - dst_ptr[0] = src_ptr[0]; - dst_ptr[1] = src_ptr[1]; - dst_ptr[2] = src_ptr[2]; - dst_ptr[3] = src_ptr[3]; - dst_ptr += 4; - src_ptr += m; - dst_ptr[0] = src_ptr[0]; - dst_ptr[1] = src_ptr[1]; - dst_ptr[2] = src_ptr[2]; - dst_ptr[3] = src_ptr[3]; - dst_ptr += 4; - src_ptr += sl_minus_m; - } -} - -// dest(i)=src -// use scalar or sse or avx? Code -// should be the inverse of reim4_extract_1col_from_reim -void reim4_save_1blk_to_reim_ref(uint64_t m, uint64_t blk, - double* dst, // 1 reim vector of length m - const double* src // 8 doubles -) { - assert(blk < (m >> 2)); - double* dst_ptr = dst + (blk << 2); - // save the real part - dst_ptr[0] = src[0]; - dst_ptr[1] = src[1]; - dst_ptr[2] = src[2]; - dst_ptr[3] = src[3]; - dst_ptr += m; - // save the imag part - dst_ptr[0] = src[4]; - dst_ptr[1] = src[5]; - dst_ptr[2] = src[6]; - dst_ptr[3] = src[7]; -} - -void reim4_add_1blk_to_reim_ref(uint64_t m, uint64_t blk, - double* dst, // 1 reim vector of length m - const double* src // 8 doubles -) { - assert(blk < (m >> 2)); - double* dst_ptr = dst + (blk << 2); - // add the real part - dst_ptr[0] += src[0]; - dst_ptr[1] += src[1]; - dst_ptr[2] += src[2]; - dst_ptr[3] += src[3]; - dst_ptr += m; - // add the imag part - dst_ptr[0] += src[4]; - dst_ptr[1] += src[5]; - dst_ptr[2] += src[6]; - dst_ptr[3] += src[7]; -} - -// dest = 0 -void reim4_zero(double* const dst // 8 doubles -) { - for (uint64_t i = 0; i < 8; ++i) dst[i] = 0; -} - -/** @brief dest = a + b */ -void reim4_add(double* const dst, // 8 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 8 doubles -) { - for (uint64_t k = 0; k < 4; ++k) { - const double a = u[k]; - const double c = v[k]; - const double b = u[k + 4]; - const double d = v[k + 4]; - - dst[k] = a + c; - dst[k + 4] = b + d; - } -} - -/** @brief dest = a * b */ -void reim4_mul(double* const dst, // 8 doubles - const double* const u, // 8 doubles - const double* const v // 8 doubles -) { - for (uint64_t k = 0; k < 4; ++k) { - const double a = u[k]; - const double c = v[k]; - const double b = u[k + 4]; - const double d = v[k + 4]; - - dst[k] = a * c - b * d; - dst[k + 4] = a * d + b * c; - } -} - -/** @brief dest += a * b */ -void reim4_add_mul(double* const dst, // 8 doubles - const double* const u, // 8 doubles - const double* const v // 8 doubles -) { - for (uint64_t k = 0; k < 4; ++k) { - const double a = u[k]; - const double c = v[k]; - const double b = u[k + 4]; - const double d = v[k + 4]; - - dst[k] += a * c - b * d; - dst[k + 4] += a * d + b * c; - } -} - -/** dest = uT * v where u is a vector of size nrows, and v is a nrows x 1 matrix */ -void reim4_vec_mat1col_product_ref(const uint64_t nrows, - double* const dst, // 8 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 8 doubles -) { - reim4_zero(dst); - - for (uint64_t i = 0, j = 0; i < nrows; ++i, j += 8) { - reim4_add_mul(dst, u + j, v + j); - } -} - -/** dest = uT * v where u is a vector of size nrows, and v is a nrows x 2 matrix */ -void reim4_vec_mat2cols_product_ref(const uint64_t nrows, - double* const dst, // 16 doubles - const double* const u, // nrows * 8 doubles - const double* const v // nrows * 16 doubles -) { - reim4_zero(dst); - reim4_zero(dst + 8); - - double* dst1 = dst; - double* dst2 = dst + 8; - - for (uint64_t i = 0, j = 0; i < nrows; ++i, j += 8) { - uint64_t double_j = j << 1; - reim4_add_mul(dst1, u + j, v + double_j); - reim4_add_mul(dst2, u + j, v + double_j + 8); - } -} - -/** - * @brief k-th coefficient of the convolution product a * b. - * - * The k-th coefficient is defined as sum_{i+j=k} a[i].b[j]. - * (in this sum, i and j must remain within the bounds [0,sizea[ and [0,sizeb[) - * - * In practice, accounting for these bounds, the convolution function boils down to - * ``` - * res := 0 - * if (k<sizeb+sizea-1) then - * for j from max(0,k+1-sizea) incl to min(sizeb,k+1) excl do - * res += a[k-j] * b[j] - * return res - * ``` - */ -EXPORT void reim4_convolution_1coeff_ref(uint64_t k, double* dest, const double* a, uint64_t sizea, const double* b, - uint64_t sizeb) { - reim4_zero(dest); - if (k >= sizea + sizeb) return; - uint64_t jmin = k >= sizea ? k + 1 - sizea : 0; - uint64_t jmax = k < sizeb ? k + 1 : sizeb; - for (uint64_t j = jmin; j < jmax; ++j) { - reim4_add_mul(dest, a + 8 * (k - j), b + 8 * j); - } -} - -/** @brief returns two consecutive convolution coefficients: k and k+1 */ -EXPORT void reim4_convolution_2coeff_ref(uint64_t k, double* dest, const double* a, uint64_t sizea, const double* b, - uint64_t sizeb) { - reim4_convolution_1coeff_ref(k, dest, a, sizea, b, sizeb); - reim4_convolution_1coeff_ref(k + 1, dest + 8, a, sizea, b, sizeb); -} - -/** - * @brief From the convolution a * b, return the coefficients between offset and offset + size - * For the full convolution, use offset=0 and size=sizea+sizeb-1. - */ -EXPORT void reim4_convolution_ref(double* dest, uint64_t dest_size, uint64_t dest_offset, const double* a, - uint64_t sizea, const double* b, uint64_t sizeb) { - for (uint64_t k = 0; k < dest_size; ++k) { - reim4_convolution_1coeff_ref(k + dest_offset, dest + 8 * k, a, sizea, b, sizeb); - } -}
@@ -1,19 +0,0 @@
-#include "reim4_fftvec_internal.h" -#include "reim4_fftvec_private.h" - -EXPORT void reim4_fftvec_addmul(const REIM4_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, - const double* b) { - tables->function(tables, r, a, b); -} - -EXPORT void reim4_fftvec_mul(const REIM4_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b) { - tables->function(tables, r, a, b); -} - -EXPORT void reim4_from_cplx(const REIM4_FROM_CPLX_PRECOMP* tables, double* r, const void* a) { - tables->function(tables, r, a); -} - -EXPORT void reim4_to_cplx(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a) { - tables->function(tables, r, a); -}
@@ -1,13 +0,0 @@
-#include "reim4_fftvec_private.h" - -EXPORT void reim4_fftvec_mul_fma(const REIM4_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, - const double* b){UNDEFINED()} - -EXPORT void reim4_fftvec_addmul_fma(const REIM4_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, - const double* b){UNDEFINED()} - -EXPORT void reim4_from_cplx_fma(const REIM4_FROM_CPLX_PRECOMP* tables, double* r, const void* a){UNDEFINED()} - -EXPORT void reim4_to_cplx_fma(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a) { - UNDEFINED() -}
@@ -1,54 +0,0 @@
-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim4_fftvec_private.h" - -EXPORT void reim4_fftvec_addmul_fma(const REIM4_FFTVEC_ADDMUL_PRECOMP* tables, double* r_ptr, const double* a_ptr, - const double* b_ptr) { - const double* const rend_ptr = r_ptr + (tables->m << 1); - while (r_ptr != rend_ptr) { - __m256d rr = _mm256_loadu_pd(r_ptr); - __m256d ri = _mm256_loadu_pd(r_ptr + 4); - const __m256d ar = _mm256_loadu_pd(a_ptr); - const __m256d ai = _mm256_loadu_pd(a_ptr + 4); - const __m256d br = _mm256_loadu_pd(b_ptr); - const __m256d bi = _mm256_loadu_pd(b_ptr + 4); - - rr = _mm256_fmsub_pd(ai, bi, rr); - rr = _mm256_fmsub_pd(ar, br, rr); - ri = _mm256_fmadd_pd(ar, bi, ri); - ri = _mm256_fmadd_pd(ai, br, ri); - - _mm256_storeu_pd(r_ptr, rr); - _mm256_storeu_pd(r_ptr + 4, ri); - - r_ptr += 8; - a_ptr += 8; - b_ptr += 8; - } -} - -EXPORT void reim4_fftvec_mul_fma(const REIM4_FFTVEC_MUL_PRECOMP* tables, double* r_ptr, const double* a_ptr, - const double* b_ptr) { - const double* const rend_ptr = r_ptr + (tables->m << 1); - while (r_ptr != rend_ptr) { - const __m256d ar = _mm256_loadu_pd(a_ptr); - const __m256d ai = _mm256_loadu_pd(a_ptr + 4); - const __m256d br = _mm256_loadu_pd(b_ptr); - const __m256d bi = _mm256_loadu_pd(b_ptr + 4); - - const __m256d t1 = _mm256_mul_pd(ai, bi); - const __m256d t2 = _mm256_mul_pd(ar, bi); - - __m256d rr = _mm256_fmsub_pd(ar, br, t1); - __m256d ri = _mm256_fmadd_pd(ai, br, t2); - - _mm256_storeu_pd(r_ptr, rr); - _mm256_storeu_pd(r_ptr + 4, ri); - - r_ptr += 8; - a_ptr += 8; - b_ptr += 8; - } -}
@@ -1,97 +0,0 @@
-#include <errno.h> -#include <memory.h> -#include <stdlib.h> -#include <string.h> - -#include "../commons_private.h" -#include "reim4_fftvec_internal.h" -#include "reim4_fftvec_private.h" - -void* init_reim4_fftvec_addmul_precomp(REIM4_FFTVEC_ADDMUL_PRECOMP* res, uint32_t m) { - res->m = m; - if (CPU_SUPPORTS("fma")) { - if (m >= 2) { - res->function = reim4_fftvec_addmul_fma; - } else { - res->function = reim4_fftvec_addmul_ref; - } - } else { - res->function = reim4_fftvec_addmul_ref; - } - return res; -} - -EXPORT REIM4_FFTVEC_ADDMUL_PRECOMP* new_reim4_fftvec_addmul_precomp(uint32_t m) { - REIM4_FFTVEC_ADDMUL_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim4_fftvec_addmul_precomp(res, m)); -} - -EXPORT void reim4_fftvec_addmul_ref(const REIM4_FFTVEC_ADDMUL_PRECOMP* precomp, double* r, const double* a, - const double* b) { - const uint64_t m = precomp->m; - for (uint64_t j = 0; j < m / 4; ++j) { - for (uint64_t i = 0; i < 4; ++i) { - double re = a[i] * b[i] - a[i + 4] * b[i + 4]; - double im = a[i] * b[i + 4] + a[i + 4] * b[i]; - r[i] += re; - r[i + 4] += im; - } - a += 8; - b += 8; - r += 8; - } -} - -EXPORT void reim4_fftvec_addmul_simple(uint32_t m, double* r, const double* a, const double* b) { - static REIM4_FFTVEC_ADDMUL_PRECOMP precomp[32]; - REIM4_FFTVEC_ADDMUL_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_reim4_fftvec_addmul_precomp(p, m)) abort(); - } - p->function(p, r, a, b); -} - -void* init_reim4_fftvec_mul_precomp(REIM4_FFTVEC_MUL_PRECOMP* res, uint32_t m) { - res->m = m; - if (CPU_SUPPORTS("fma")) { - if (m >= 4) { - res->function = reim4_fftvec_mul_fma; - } else { - res->function = reim4_fftvec_mul_ref; - } - } else { - res->function = reim4_fftvec_mul_ref; - } - return res; -} - -EXPORT REIM4_FFTVEC_MUL_PRECOMP* new_reim4_fftvec_mul_precomp(uint32_t m) { - REIM4_FFTVEC_MUL_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim4_fftvec_mul_precomp(res, m)); -} - -EXPORT void reim4_fftvec_mul_ref(const REIM4_FFTVEC_MUL_PRECOMP* precomp, double* r, const double* a, const double* b) { - const uint64_t m = precomp->m; - for (uint64_t j = 0; j < m / 4; ++j) { - for (uint64_t i = 0; i < 4; ++i) { - double re = a[i] * b[i] - a[i + 4] * b[i + 4]; - double im = a[i] * b[i + 4] + a[i + 4] * b[i]; - r[i] = re; - r[i + 4] = im; - } - a += 8; - b += 8; - r += 8; - } -} - -EXPORT void reim4_fftvec_mul_simple(uint32_t m, double* r, const double* a, const double* b) { - static REIM4_FFTVEC_MUL_PRECOMP precomp[32]; - REIM4_FFTVEC_MUL_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_reim4_fftvec_mul_precomp(p, m)) abort(); - } - p->function(p, r, a, b); -}
@@ -1,37 +0,0 @@
-#include <assert.h> -#include <immintrin.h> -#include <stdint.h> - -#include "reim4_fftvec_private.h" - -EXPORT void reim4_from_cplx_fma(const REIM4_FROM_CPLX_PRECOMP* tables, double* r_ptr, const void* a) { - const double* const rend_ptr = r_ptr + (tables->m << 1); - - const double* a_ptr = (double*)a; - while (r_ptr != rend_ptr) { - __m256d t1 = _mm256_loadu_pd(a_ptr); - __m256d t2 = _mm256_loadu_pd(a_ptr + 4); - - _mm256_storeu_pd(r_ptr, _mm256_unpacklo_pd(t1, t2)); - _mm256_storeu_pd(r_ptr + 4, _mm256_unpackhi_pd(t1, t2)); - - r_ptr += 8; - a_ptr += 8; - } -} - -EXPORT void reim4_to_cplx_fma(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a_ptr) { - const double* const aend_ptr = a_ptr + (tables->m << 1); - double* r_ptr = (double*)r; - - while (a_ptr != aend_ptr) { - __m256d t1 = _mm256_loadu_pd(a_ptr); - __m256d t2 = _mm256_loadu_pd(a_ptr + 4); - - _mm256_storeu_pd(r_ptr, _mm256_unpacklo_pd(t1, t2)); - _mm256_storeu_pd(r_ptr + 4, _mm256_unpackhi_pd(t1, t2)); - - r_ptr += 8; - a_ptr += 8; - } -}
@@ -1,116 +0,0 @@
-#include <errno.h> -#include <memory.h> -#include <stdlib.h> -#include <string.h> - -#include "../commons_private.h" -#include "reim4_fftvec_internal.h" -#include "reim4_fftvec_private.h" - -EXPORT void reim4_from_cplx_ref(const REIM4_FROM_CPLX_PRECOMP* tables, double* r, const void* a) { - const double* x = (double*)a; - const uint64_t m = tables->m; - for (uint64_t i = 0; i < m / 4; ++i) { - double r0 = x[0]; - double i0 = x[1]; - double r1 = x[2]; - double i1 = x[3]; - double r2 = x[4]; - double i2 = x[5]; - double r3 = x[6]; - double i3 = x[7]; - r[0] = r0; - r[1] = r2; - r[2] = r1; - r[3] = r3; - r[4] = i0; - r[5] = i2; - r[6] = i1; - r[7] = i3; - x += 8; - r += 8; - } -} - -void* init_reim4_from_cplx_precomp(REIM4_FROM_CPLX_PRECOMP* res, uint32_t nn) { - res->m = nn / 2; - if (CPU_SUPPORTS("fma")) { - if (nn >= 4) { - res->function = reim4_from_cplx_fma; - } else { - res->function = reim4_from_cplx_ref; - } - } else { - res->function = reim4_from_cplx_ref; - } - return res; -} - -EXPORT REIM4_FROM_CPLX_PRECOMP* new_reim4_from_cplx_precomp(uint32_t m) { - REIM4_FROM_CPLX_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim4_from_cplx_precomp(res, m)); -} - -EXPORT void reim4_from_cplx_simple(uint32_t m, double* r, const void* a) { - static REIM4_FROM_CPLX_PRECOMP precomp[32]; - REIM4_FROM_CPLX_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_reim4_from_cplx_precomp(p, m)) abort(); - } - p->function(p, r, a); -} - -EXPORT void reim4_to_cplx_ref(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a) { - double* y = (double*)r; - const uint64_t m = tables->m; - for (uint64_t i = 0; i < m / 4; ++i) { - double r0 = a[0]; - double r2 = a[1]; - double r1 = a[2]; - double r3 = a[3]; - double i0 = a[4]; - double i2 = a[5]; - double i1 = a[6]; - double i3 = a[7]; - y[0] = r0; - y[1] = i0; - y[2] = r1; - y[3] = i1; - y[4] = r2; - y[5] = i2; - y[6] = r3; - y[7] = i3; - a += 8; - y += 8; - } -} - -void* init_reim4_to_cplx_precomp(REIM4_TO_CPLX_PRECOMP* res, uint32_t m) { - res->m = m; - if (CPU_SUPPORTS("fma")) { - if (m >= 2) { - res->function = reim4_to_cplx_fma; - } else { - res->function = reim4_to_cplx_ref; - } - } else { - res->function = reim4_to_cplx_ref; - } - return res; -} - -EXPORT REIM4_TO_CPLX_PRECOMP* new_reim4_to_cplx_precomp(uint32_t m) { - REIM4_TO_CPLX_PRECOMP* res = malloc(sizeof(*res)); - if (!res) return spqlios_error(strerror(errno)); - return spqlios_keep_or_free(res, init_reim4_to_cplx_precomp(res, m)); -} - -EXPORT void reim4_to_cplx_simple(uint32_t m, void* r, const double* a) { - static REIM4_TO_CPLX_PRECOMP precomp[32]; - REIM4_TO_CPLX_PRECOMP* p = precomp + log2m(m); - if (!p->function) { - if (!init_reim4_to_cplx_precomp(p, m)) abort(); - } - p->function(p, r, a); -}
@@ -1,20 +0,0 @@
-#ifndef SPQLIOS_REIM4_FFTVEC_INTERNAL_H -#define SPQLIOS_REIM4_FFTVEC_INTERNAL_H - -#include "reim4_fftvec_public.h" - -EXPORT void reim4_fftvec_mul_ref(const REIM4_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b); -EXPORT void reim4_fftvec_mul_fma(const REIM4_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b); - -EXPORT void reim4_fftvec_addmul_ref(const REIM4_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, - const double* b); -EXPORT void reim4_fftvec_addmul_fma(const REIM4_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, - const double* b); - -EXPORT void reim4_from_cplx_ref(const REIM4_FROM_CPLX_PRECOMP* tables, double* r, const void* a); -EXPORT void reim4_from_cplx_fma(const REIM4_FROM_CPLX_PRECOMP* tables, double* r, const void* a); - -EXPORT void reim4_to_cplx_ref(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a); -EXPORT void reim4_to_cplx_fma(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a); - -#endif // SPQLIOS_REIM4_FFTVEC_INTERNAL_H
@@ -1,33 +0,0 @@
-#ifndef SPQLIOS_REIM4_FFTVEC_PRIVATE_H -#define SPQLIOS_REIM4_FFTVEC_PRIVATE_H - -#include "reim4_fftvec_public.h" - -#define STATIC_ASSERT(condition) (void)sizeof(char[-1 + 2 * !!(condition)]) - -typedef void (*R4_FFTVEC_MUL_FUNC)(const REIM4_FFTVEC_MUL_PRECOMP*, double*, const double*, const double*); -typedef void (*R4_FFTVEC_ADDMUL_FUNC)(const REIM4_FFTVEC_ADDMUL_PRECOMP*, double*, const double*, const double*); -typedef void (*R4_FROM_CPLX_FUNC)(const REIM4_FROM_CPLX_PRECOMP*, double*, const void*); -typedef void (*R4_TO_CPLX_FUNC)(const REIM4_TO_CPLX_PRECOMP*, void*, const double*); - -struct reim4_mul_precomp { - R4_FFTVEC_MUL_FUNC function; - int64_t m; -}; - -struct reim4_addmul_precomp { - R4_FFTVEC_ADDMUL_FUNC function; - int64_t m; -}; - -struct reim4_from_cplx_precomp { - R4_FROM_CPLX_FUNC function; - int64_t m; -}; - -struct reim4_to_cplx_precomp { - R4_TO_CPLX_FUNC function; - int64_t m; -}; - -#endif // SPQLIOS_REIM4_FFTVEC_PRIVATE_H
@@ -1,59 +0,0 @@
-#ifndef SPQLIOS_REIM4_FFTVEC_PUBLIC_H -#define SPQLIOS_REIM4_FFTVEC_PUBLIC_H - -#include "../commons.h" - -typedef struct reim4_addmul_precomp REIM4_FFTVEC_ADDMUL_PRECOMP; -typedef struct reim4_mul_precomp REIM4_FFTVEC_MUL_PRECOMP; -typedef struct reim4_from_cplx_precomp REIM4_FROM_CPLX_PRECOMP; -typedef struct reim4_to_cplx_precomp REIM4_TO_CPLX_PRECOMP; - -EXPORT REIM4_FFTVEC_MUL_PRECOMP* new_reim4_fftvec_mul_precomp(uint32_t m); -EXPORT void reim4_fftvec_mul(const REIM4_FFTVEC_MUL_PRECOMP* tables, double* r, const double* a, const double* b); -#define delete_reim4_fftvec_mul_precomp free - -EXPORT REIM4_FFTVEC_ADDMUL_PRECOMP* new_reim4_fftvec_addmul_precomp(uint32_t nn); -EXPORT void reim4_fftvec_addmul(const REIM4_FFTVEC_ADDMUL_PRECOMP* tables, double* r, const double* a, const double* b); -#define delete_reim4_fftvec_addmul_precomp free - -/** - * @brief prepares a conversion from the cplx fftvec layout to the reim4 layout. - * @param m complex dimension m from C[X] mod X^m-i. - */ -EXPORT REIM4_FROM_CPLX_PRECOMP* new_reim4_from_cplx_precomp(uint32_t m); -EXPORT void reim4_from_cplx(const REIM4_FROM_CPLX_PRECOMP* tables, double* r, const void* a); -#define delete_reim4_from_cplx_precomp free - -/** - * @brief prepares a conversion from the reim4 fftvec layout to the cplx layout - * @param m the complex dimension m from C[X] mod X^m-i. - */ -EXPORT REIM4_TO_CPLX_PRECOMP* new_reim4_to_cplx_precomp(uint32_t m); -EXPORT void reim4_to_cplx(const REIM4_TO_CPLX_PRECOMP* tables, void* r, const double* a); -#define delete_reim4_to_cplx_precomp free - -/** - * @brief Simpler API for the fftvec multiplication function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim4_fftvec_mul_simple(uint32_t m, double* r, const double* a, const double* b); - -/** - * @brief Simpler API for the fftvec addmul function. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim4_fftvec_addmul_simple(uint32_t m, double* r, const double* a, const double* b); - -/** - * @brief Simpler API for cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim4_from_cplx_simple(uint32_t m, double* r, const void* a); - -/** - * @brief Simpler API for to cplx conversion. - * For each dimension, the precomputed tables for this dimension are generated automatically the first time. - * It is advised to do one dry-run call per desired dimension before using in a multithread environment */ -EXPORT void reim4_to_cplx_simple(uint32_t m, void* r, const double* a); - -#endif // SPQLIOS_REIM4_FFTVEC_PUBLIC_H
@@ -1,142 +0,0 @@
-set(CMAKE_CXX_STANDARD 17) - -set(test_incs ..) -set(gtest_libs) -set(benchmark_libs) -# searching for libgtest -find_path(gtest_inc NAMES gtest/gtest.h) -find_library(gtest NAMES gtest) -find_library(gtest_main REQUIRED NAMES gtest_main) -if (gtest_inc AND gtest AND gtest_main) - message(STATUS "Found gtest: I=${gtest_inc} L=${gtest},${gtest_main}") - set(test_incs ${test_incs} ${gtest_inc}) - set(gtest_libs ${gtest_libs} ${gtest} ${gtest_main} pthread) -else() - message(FATAL_ERROR "Libgtest not found (required if ENABLE_TESTING is on): I=${gtest_inc} L=${gtest},${gtest_main}") -endif() -# searching for libbenchmark -find_path(benchmark_inc NAMES benchmark/benchmark.h) -find_library(benchmark NAMES benchmark) -if (benchmark_inc AND benchmark) - message(STATUS "Found benchmark: I=${benchmark_inc} L=${benchmark}") - set(test_incs ${test_incs} ${benchmark_inc}) - set(benchmark_libs ${benchmark_libs} ${benchmark}) -else() - message(FATAL_ERROR "Libbenchmark not found (required if ENABLE_TESTING is on): I=${benchmark_inc} L=${benchmark}") -endif() -find_path(VALGRIND_DIR NAMES valgrind/valgrind.h) -if (VALGRIND_DIR) - message(STATUS "Found valgrind header ${VALGRIND_DIR}") -else () - # for now, we will fail if we don't find valgrind for tests - message(STATUS "CANNOT FIND valgrind header: ${VALGRIND_DIR}") -endif () - -add_library(spqlios-testlib SHARED - testlib/random.cpp - testlib/test_commons.h - testlib/test_commons.cpp - testlib/mod_q120.h - testlib/mod_q120.cpp - testlib/negacyclic_polynomial.cpp - testlib/negacyclic_polynomial.h - testlib/negacyclic_polynomial_impl.h - testlib/reim4_elem.cpp - testlib/reim4_elem.h - testlib/fft64_dft.cpp - testlib/fft64_dft.h - testlib/fft64_layouts.h - testlib/fft64_layouts.cpp - testlib/ntt120_layouts.cpp - testlib/ntt120_layouts.h - testlib/ntt120_dft.cpp - testlib/ntt120_dft.h - testlib/test_hash.cpp - testlib/sha3.h - testlib/sha3.c - testlib/polynomial_vector.h - testlib/polynomial_vector.cpp - testlib/vec_rnx_layout.h - testlib/vec_rnx_layout.cpp - testlib/zn_layouts.h - testlib/zn_layouts.cpp -) -if (VALGRIND_DIR) - target_include_directories(spqlios-testlib PRIVATE ${VALGRIND_DIR}) - target_compile_definitions(spqlios-testlib PRIVATE VALGRIND_MEM_TESTS) -endif () -target_link_libraries(spqlios-testlib libspqlios) - - - -# main unittest file -message(STATUS "${gtest_libs}") -set(UNITTEST_FILES - spqlios_test.cpp - spqlios_reim_conversions_test.cpp - spqlios_reim_test.cpp - spqlios_reim4_arithmetic_test.cpp - spqlios_cplx_test.cpp - spqlios_cplx_conversions_test.cpp - spqlios_q120_ntt_test.cpp - spqlios_q120_arithmetic_test.cpp - spqlios_coeffs_arithmetic_test.cpp - spqlios_vec_znx_big_test.cpp - spqlios_znx_small_test.cpp - spqlios_vmp_product_test.cpp - spqlios_vec_znx_dft_test.cpp - spqlios_svp_apply_test.cpp - spqlios_svp_prepare_test.cpp - spqlios_vec_znx_test.cpp - spqlios_vec_rnx_test.cpp - spqlios_vec_rnx_vmp_test.cpp - spqlios_vec_rnx_conversions_test.cpp - spqlios_vec_rnx_ppol_test.cpp - spqlios_vec_rnx_approxdecomp_tnxdbl_test.cpp - spqlios_zn_approxdecomp_test.cpp - spqlios_zn_conversions_test.cpp - spqlios_zn_vmp_test.cpp - - -) - -add_executable(spqlios-test ${UNITTEST_FILES}) -target_link_libraries(spqlios-test spqlios-testlib libspqlios ${gtest_libs}) -target_include_directories(spqlios-test PRIVATE ${test_incs}) -add_test(NAME spqlios-test COMMAND spqlios-test) -if (WIN32) - # copy the dlls to the test directory - cmake_minimum_required(VERSION 3.26) - add_custom_command( - POST_BUILD - TARGET spqlios-test - COMMAND ${CMAKE_COMMAND} -E copy - -t $<TARGET_FILE_DIR:spqlios-test> $<TARGET_RUNTIME_DLLS:spqlios-testlib> $<TARGET_RUNTIME_DLLS:libspqlios> - COMMAND_EXPAND_LISTS - ) -endif() - -# benchmarks -add_executable(spqlios-cplx-fft-bench spqlios_cplx_fft_bench.cpp) -target_link_libraries(spqlios-cplx-fft-bench libspqlios ${benchmark_libs} pthread) -target_include_directories(spqlios-cplx-fft-bench PRIVATE ${test_incs}) - -if (X86 OR X86_WIN32) - add_executable(spqlios-q120-ntt-bench spqlios_q120_ntt_bench.cpp) - target_link_libraries(spqlios-q120-ntt-bench libspqlios ${benchmark_libs} pthread) - target_include_directories(spqlios-q120-ntt-bench PRIVATE ${test_incs}) - - add_executable(spqlios-q120-arithmetic-bench spqlios_q120_arithmetic_bench.cpp) - target_link_libraries(spqlios-q120-arithmetic-bench libspqlios ${benchmark_libs} pthread) - target_include_directories(spqlios-q120-arithmetic-bench PRIVATE ${test_incs}) -endif () - -if (X86 OR X86_WIN32) - add_executable(spqlios_reim4_arithmetic_bench spqlios_reim4_arithmetic_bench.cpp) - target_link_libraries(spqlios_reim4_arithmetic_bench ${benchmark_libs} libspqlios pthread) - target_include_directories(spqlios_reim4_arithmetic_bench PRIVATE ${test_incs}) -endif () - -if (DEVMODE_INSTALL) - install(TARGETS spqlios-testlib) -endif()
@@ -1,488 +0,0 @@
-#include <gtest/gtest.h> -#include <sys/types.h> - -#include <cstdint> -#include <limits> -#include <random> -#include <vector> - -#include "../spqlios/coeffs/coeffs_arithmetic.h" -#include "test/testlib/mod_q120.h" -#include "testlib/negacyclic_polynomial.h" -#include "testlib/test_commons.h" - -/// tests of element-wise operations -template <typename T, typename F, typename G> -void test_elemw_op(F elemw_op, G poly_elemw_op) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> a = polynomial<T>::random(n); - polynomial<T> b = polynomial<T>::random(n); - polynomial<T> expect(n); - polynomial<T> actual(n); - // out of place - expect = poly_elemw_op(a, b); - elemw_op(n, actual.data(), a.data(), b.data()); - ASSERT_EQ(actual, expect); - // in place 1 - actual = polynomial<T>::random(n); - expect = poly_elemw_op(actual, b); - elemw_op(n, actual.data(), actual.data(), b.data()); - ASSERT_EQ(actual, expect); - // in place 2 - actual = polynomial<T>::random(n); - expect = poly_elemw_op(a, actual); - elemw_op(n, actual.data(), a.data(), actual.data()); - ASSERT_EQ(actual, expect); - // in place 3 - actual = polynomial<T>::random(n); - expect = poly_elemw_op(actual, actual); - elemw_op(n, actual.data(), actual.data(), actual.data()); - ASSERT_EQ(actual, expect); - } -} - -static polynomial<int64_t> poly_i64_add(const polynomial<int64_t>& u, polynomial<int64_t>& v) { return u + v; } -static polynomial<int64_t> poly_i64_sub(const polynomial<int64_t>& u, polynomial<int64_t>& v) { return u - v; } -TEST(coeffs_arithmetic, znx_add_i64_ref) { test_elemw_op<int64_t>(znx_add_i64_ref, poly_i64_add); } -TEST(coeffs_arithmetic, znx_sub_i64_ref) { test_elemw_op<int64_t>(znx_sub_i64_ref, poly_i64_sub); } -#ifdef __x86_64__ -TEST(coeffs_arithmetic, znx_add_i64_avx) { test_elemw_op<int64_t>(znx_add_i64_avx, poly_i64_add); } -TEST(coeffs_arithmetic, znx_sub_i64_avx) { test_elemw_op<int64_t>(znx_sub_i64_avx, poly_i64_sub); } -#endif - -/// tests of element-wise operations -template <typename T, typename F, typename G> -void test_elemw_unary_op(F elemw_op, G poly_elemw_op) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> a = polynomial<T>::random(n); - polynomial<T> expect(n); - polynomial<T> actual(n); - // out of place - expect = poly_elemw_op(a); - elemw_op(n, actual.data(), a.data()); - ASSERT_EQ(actual, expect); - // in place - actual = polynomial<T>::random(n); - expect = poly_elemw_op(actual); - elemw_op(n, actual.data(), actual.data()); - ASSERT_EQ(actual, expect); - } -} - -static polynomial<int64_t> poly_i64_neg(const polynomial<int64_t>& u) { return -u; } -static polynomial<int64_t> poly_i64_copy(const polynomial<int64_t>& u) { return u; } -TEST(coeffs_arithmetic, znx_neg_i64_ref) { test_elemw_unary_op<int64_t>(znx_negate_i64_ref, poly_i64_neg); } -TEST(coeffs_arithmetic, znx_copy_i64_ref) { test_elemw_unary_op<int64_t>(znx_copy_i64_ref, poly_i64_copy); } -#ifdef __x86_64__ -TEST(coeffs_arithmetic, znx_neg_i64_avx) { test_elemw_unary_op<int64_t>(znx_negate_i64_avx, poly_i64_neg); } -#endif - -/// tests of the rotations out of place -template <typename T, typename F> -void test_rotation_outplace(F rotate) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> poly = polynomial<T>::random(n); - polynomial<T> expect(n); - polynomial<T> actual(n); - for (uint64_t trial = 0; trial < 10; ++trial) { - int64_t p = uniform_i64_bits(32); - // rotate by p - for (uint64_t i = 0; i < n; ++i) { - expect.set_coeff(i, poly.get_coeff(i - p)); - } - // rotate using the function - rotate(n, p, actual.data(), poly.data()); - ASSERT_EQ(actual, expect); - } - } -} - -TEST(coeffs_arithmetic, rnx_rotate_f64) { test_rotation_outplace<double>(rnx_rotate_f64); } -TEST(coeffs_arithmetic, znx_rotate_i64) { test_rotation_outplace<int64_t>(znx_rotate_i64); } - -/// tests of the rotations out of place -template <typename T, typename F> -void test_rotation_inplace(F rotate) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> poly = polynomial<T>::random(n); - polynomial<T> expect(n); - for (uint64_t trial = 0; trial < 10; ++trial) { - polynomial<T> actual = poly; - int64_t p = uniform_i64_bits(32); - // rotate by p - for (uint64_t i = 0; i < n; ++i) { - expect.set_coeff(i, poly.get_coeff(i - p)); - } - // rotate using the function - rotate(n, p, actual.data()); - ASSERT_EQ(actual, expect); - } - } -} - -TEST(coeffs_arithmetic, rnx_rotate_inplace_f64) { test_rotation_inplace<double>(rnx_rotate_inplace_f64); } - -TEST(coeffs_arithmetic, znx_rotate_inplace_i64) { test_rotation_inplace<int64_t>(znx_rotate_inplace_i64); } - -/// tests of the rotations out of place -template <typename T, typename F> -void test_mul_xp_minus_one_outplace(F rotate) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> poly = polynomial<T>::random(n); - polynomial<T> expect(n); - polynomial<T> actual(n); - for (uint64_t trial = 0; trial < 10; ++trial) { - int64_t p = uniform_i64_bits(32); - // rotate by p - for (uint64_t i = 0; i < n; ++i) { - expect.set_coeff(i, poly.get_coeff(i - p) - poly.get_coeff(i)); - } - // rotate using the function - rotate(n, p, actual.data(), poly.data()); - ASSERT_EQ(actual, expect); - } - } -} - -TEST(coeffs_arithmetic, rnx_mul_xp_minus_one_f64) { test_mul_xp_minus_one_outplace<double>(rnx_mul_xp_minus_one_f64); } -TEST(coeffs_arithmetic, znx_mul_xp_minus_one_i64) { test_mul_xp_minus_one_outplace<int64_t>(znx_mul_xp_minus_one_i64); } - -/// tests of the rotations out of place -template <typename T, typename F> -void test_mul_xp_minus_one_inplace(F rotate) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> poly = polynomial<T>::random(n); - polynomial<T> expect(n); - for (uint64_t trial = 0; trial < 10; ++trial) { - polynomial<T> actual = poly; - int64_t p = uniform_i64_bits(32); - // rotate by p - for (uint64_t i = 0; i < n; ++i) { - expect.set_coeff(i, poly.get_coeff(i - p) - poly.get_coeff(i)); - } - // rotate using the function - rotate(n, p, actual.data()); - ASSERT_EQ(actual, expect); - } - } -} - -TEST(coeffs_arithmetic, rnx_mul_xp_minus_one_inplace_f64) { - test_mul_xp_minus_one_inplace<double>(rnx_mul_xp_minus_one_inplace_f64); -} - -// TEST(coeffs_arithmetic, znx_mul_xp_minus_one_inplace_i64) { -// test_mul_xp_minus_one_inplace<int64_t>(znx_rotate_inplace_i64); } -/// tests of the automorphisms out of place -template <typename T, typename F> -void test_automorphism_outplace(F automorphism) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<T> poly = polynomial<T>::random(n); - polynomial<T> expect(n); - polynomial<T> actual(n); - for (uint64_t trial = 0; trial < 10; ++trial) { - int64_t p = uniform_i64_bits(32) | int64_t(1); // make it odd - // automorphism p - for (uint64_t i = 0; i < n; ++i) { - expect.set_coeff(i * p, poly.get_coeff(i)); - } - // rotate using the function - automorphism(n, p, actual.data(), poly.data()); - ASSERT_EQ(actual, expect); - } - } -} - -TEST(coeffs_arithmetic, rnx_automorphism_f64) { test_automorphism_outplace<double>(rnx_automorphism_f64); } -TEST(coeffs_arithmetic, znx_automorphism_i64) { test_automorphism_outplace<int64_t>(znx_automorphism_i64); } - -/// tests of the automorphisms out of place -template <typename T, typename F> -void test_automorphism_inplace(F automorphism) { - for (uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096, 16384}) { - polynomial<T> poly = polynomial<T>::random(n); - polynomial<T> expect(n); - for (uint64_t trial = 0; trial < 20; ++trial) { - polynomial<T> actual = poly; - int64_t p = uniform_i64_bits(32) | int64_t(1); // make it odd - // automorphism p - for (uint64_t i = 0; i < n; ++i) { - expect.set_coeff(i * p, poly.get_coeff(i)); - } - automorphism(n, p, actual.data()); - if (!(actual == expect)) { - std::cerr << "automorphism p: " << p << std::endl; - for (uint64_t i = 0; i < n; ++i) { - std::cerr << i << " " << actual.get_coeff(i) << " vs " << expect.get_coeff(i) << " " - << (actual.get_coeff(i) == expect.get_coeff(i)) << std::endl; - } - } - ASSERT_EQ(actual, expect); - } - } -} -TEST(coeffs_arithmetic, rnx_automorphism_inplace_f64) { - test_automorphism_inplace<double>(rnx_automorphism_inplace_f64); -} -TEST(coeffs_arithmetic, znx_automorphism_inplace_i64) { - test_automorphism_inplace<int64_t>(znx_automorphism_inplace_i64); -} - -// TODO: write a test later! - -/** - * @brief res = (X^p-1).in - * @param nn the ring dimension - * @param p must be between -2nn <= p <= 2nn - * @param in is a rnx/znx vector of dimension nn - */ -EXPORT void rnx_mul_xp_minus_one(uint64_t nn, int64_t p, double* res, const double* in); -EXPORT void znx_mul_xp_minus_one(uint64_t nn, int64_t p, int64_t* res, const int64_t* in); - -// normalize with no carry in nor carry out -template <uint8_t inplace_flag, typename F> -void test_znx_normalize(F normalize) { - for (const uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<int64_t> inp = znx_i64::random_log2bound(n, 62); - if (n >= 2) { - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, (INT64_C(1) << 62)); - } - for (const uint64_t base_k : {2, 3, 19, 35, 62}) { - polynomial<int64_t> out; - int64_t* inp_ptr; - if (inplace_flag == 1) { - out = polynomial<int64_t>(inp); - inp_ptr = out.data(); - } else { - out = polynomial<int64_t>(n); - inp_ptr = inp.data(); - } - - znx_normalize(n, base_k, out.data(), nullptr, inp_ptr, nullptr); - for (uint64_t i = 0; i < n; ++i) { - const int64_t x = inp.get_coeff(i); - const int64_t y = out.get_coeff(i); - const int64_t y_exp = centermod(x, INT64_C(1) << base_k); - ASSERT_EQ(y, y_exp) << n << " " << base_k << " " << i << " " << x << " " << y; - } - } - } -} - -TEST(coeffs_arithmetic, znx_normalize_outplace) { test_znx_normalize<0>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_inplace) { test_znx_normalize<1>(znx_normalize); } - -// normalize with no carry in nor carry out -template <uint8_t inplace_flag, bool has_output, typename F> -void test_znx_normalize_cout(F normalize) { - static_assert(inplace_flag < 3, "either out or cout can be inplace with inp"); - for (const uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<int64_t> inp = znx_i64::random_log2bound(n, 62); - if (n >= 2) { - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, (INT64_C(1) << 62)); - } - for (const uint64_t base_k : {2, 3, 19, 35, 62}) { - polynomial<int64_t> out, cout; - int64_t* inp_ptr; - if (inplace_flag == 1) { - // out and inp are the same - out = polynomial<int64_t>(inp); - inp_ptr = out.data(); - cout = polynomial<int64_t>(n); - } else if (inplace_flag == 2) { - // carry out and inp are the same - cout = polynomial<int64_t>(inp); - inp_ptr = cout.data(); - out = polynomial<int64_t>(n); - } else { - // inp, out and carry out are distinct - out = polynomial<int64_t>(n); - cout = polynomial<int64_t>(n); - inp_ptr = inp.data(); - } - - znx_normalize(n, base_k, has_output ? out.data() : nullptr, cout.data(), inp_ptr, nullptr); - for (uint64_t i = 0; i < n; ++i) { - const int64_t x = inp.get_coeff(i); - const int64_t co = cout.get_coeff(i); - const int64_t y_exp = centermod((int64_t)x, INT64_C(1) << base_k); - const int64_t co_exp = (x - y_exp) >> base_k; - ASSERT_EQ(co, co_exp); - - if (has_output) { - const int64_t y = out.get_coeff(i); - ASSERT_EQ(y, y_exp); - } - } - } - } -} - -TEST(coeffs_arithmetic, znx_normalize_cout_outplace) { test_znx_normalize_cout<0, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cout_outplace) { test_znx_normalize_cout<0, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cout_inplace1) { test_znx_normalize_cout<1, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cout_inplace1) { test_znx_normalize_cout<1, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cout_inplace2) { test_znx_normalize_cout<2, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cout_inplace2) { test_znx_normalize_cout<2, true>(znx_normalize); } - -// normalize with no carry in nor carry out -template <uint8_t inplace_flag, typename F> -void test_znx_normalize_cin(F normalize) { - static_assert(inplace_flag < 3, "either inp or cin can be inplace with out"); - for (const uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<int64_t> inp = znx_i64::random_log2bound(n, 62); - if (n >= 4) { - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, -(INT64_C(1) << 62)); - inp.set_coeff(2, (INT64_C(1) << 62)); - inp.set_coeff(3, (INT64_C(1) << 62)); - } - for (const uint64_t base_k : {2, 3, 19, 35, 62}) { - polynomial<int64_t> cin = znx_i64::random_log2bound(n, 62); - if (n >= 4) { - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, (INT64_C(1) << 62)); - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, (INT64_C(1) << 62)); - } - - polynomial<int64_t> out; - int64_t *inp_ptr, *cin_ptr; - if (inplace_flag == 1) { - // out and inp are the same - out = polynomial<int64_t>(inp); - inp_ptr = out.data(); - cin_ptr = cin.data(); - } else if (inplace_flag == 2) { - // out and carry in are the same - out = polynomial<int64_t>(cin); - inp_ptr = inp.data(); - cin_ptr = out.data(); - } else { - // inp, carry in and out are distinct - out = polynomial<int64_t>(n); - inp_ptr = inp.data(); - cin_ptr = cin.data(); - } - - znx_normalize(n, base_k, out.data(), nullptr, inp_ptr, cin_ptr); - for (uint64_t i = 0; i < n; ++i) { - const int64_t x = inp.get_coeff(i); - const int64_t ci = cin.get_coeff(i); - const int64_t y = out.get_coeff(i); - - const __int128_t xp = (__int128_t)x + ci; - const int64_t y_exp = centermod((int64_t)xp, INT64_C(1) << base_k); - - ASSERT_EQ(y, y_exp) << n << " " << base_k << " " << i << " " << x << " " << y << " " << ci; - } - } - } -} - -TEST(coeffs_arithmetic, znx_normalize_cin_outplace) { test_znx_normalize_cin<0>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_inplace1) { test_znx_normalize_cin<1>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_inplace2) { test_znx_normalize_cin<2>(znx_normalize); } - -// normalize with no carry in nor carry out -template <uint8_t inplace_flag, bool has_output, typename F> -void test_znx_normalize_cin_cout(F normalize) { - static_assert(inplace_flag < 7, "either inp or cin can be inplace with out"); - for (const uint64_t n : {1, 2, 4, 8, 16, 64, 256, 4096}) { - polynomial<int64_t> inp = znx_i64::random_log2bound(n, 62); - if (n >= 4) { - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, -(INT64_C(1) << 62)); - inp.set_coeff(2, (INT64_C(1) << 62)); - inp.set_coeff(3, (INT64_C(1) << 62)); - } - for (const uint64_t base_k : {2, 3, 19, 35, 62}) { - polynomial<int64_t> cin = znx_i64::random_log2bound(n, 62); - if (n >= 4) { - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, (INT64_C(1) << 62)); - inp.set_coeff(0, -(INT64_C(1) << 62)); - inp.set_coeff(1, (INT64_C(1) << 62)); - } - - polynomial<int64_t> out, cout; - int64_t *inp_ptr, *cin_ptr; - if (inplace_flag == 1) { - // out == inp - out = polynomial<int64_t>(inp); - cout = polynomial<int64_t>(n); - inp_ptr = out.data(); - cin_ptr = cin.data(); - } else if (inplace_flag == 2) { - // cout == inp - out = polynomial<int64_t>(n); - cout = polynomial<int64_t>(inp); - inp_ptr = cout.data(); - cin_ptr = cin.data(); - } else if (inplace_flag == 3) { - // out == cin - out = polynomial<int64_t>(cin); - cout = polynomial<int64_t>(n); - inp_ptr = inp.data(); - cin_ptr = out.data(); - } else if (inplace_flag == 4) { - // cout == cin - out = polynomial<int64_t>(n); - cout = polynomial<int64_t>(cin); - inp_ptr = inp.data(); - cin_ptr = cout.data(); - } else if (inplace_flag == 5) { - // out == inp, cout == cin - out = polynomial<int64_t>(inp); - cout = polynomial<int64_t>(cin); - inp_ptr = out.data(); - cin_ptr = cout.data(); - } else if (inplace_flag == 6) { - // out == cin, cout == inp - out = polynomial<int64_t>(cin); - cout = polynomial<int64_t>(inp); - inp_ptr = cout.data(); - cin_ptr = out.data(); - } else { - out = polynomial<int64_t>(n); - cout = polynomial<int64_t>(n); - inp_ptr = inp.data(); - cin_ptr = cin.data(); - } - - znx_normalize(n, base_k, has_output ? out.data() : nullptr, cout.data(), inp_ptr, cin_ptr); - for (uint64_t i = 0; i < n; ++i) { - const int64_t x = inp.get_coeff(i); - const int64_t ci = cin.get_coeff(i); - const int64_t co = cout.get_coeff(i); - - const __int128_t xp = (__int128_t)x + ci; - const int64_t y_exp = centermod((int64_t)xp, INT64_C(1) << base_k); - const int64_t co_exp = (xp - y_exp) >> base_k; - ASSERT_EQ(co, co_exp); - - if (has_output) { - const int64_t y = out.get_coeff(i); - ASSERT_EQ(y, y_exp); - } - } - } - } -} - -TEST(coeffs_arithmetic, znx_normalize_cin_cout_outplace) { test_znx_normalize_cin_cout<0, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_outplace) { test_znx_normalize_cin_cout<0, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_cout_inplace1) { test_znx_normalize_cin_cout<1, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_inplace1) { test_znx_normalize_cin_cout<1, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_cout_inplace2) { test_znx_normalize_cin_cout<2, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_inplace2) { test_znx_normalize_cin_cout<2, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_cout_inplace3) { test_znx_normalize_cin_cout<3, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_inplace3) { test_znx_normalize_cin_cout<3, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_cout_inplace4) { test_znx_normalize_cin_cout<4, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_inplace4) { test_znx_normalize_cin_cout<4, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_cout_inplace5) { test_znx_normalize_cin_cout<5, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_inplace5) { test_znx_normalize_cin_cout<5, true>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_cin_cout_inplace6) { test_znx_normalize_cin_cout<6, false>(znx_normalize); } -TEST(coeffs_arithmetic, znx_normalize_out_cin_cout_inplace6) { test_znx_normalize_cin_cout<6, true>(znx_normalize); }
@@ -1,86 +0,0 @@
-#include <gtest/gtest.h> - -#include <cmath> - -#include "spqlios/cplx/cplx_fft_internal.h" -#include "spqlios/cplx/cplx_fft_private.h" - -#ifdef __x86_64__ -TEST(fft, cplx_from_znx32_ref_vs_fma) { - const uint32_t m = 128; - int32_t* src = (int32_t*)spqlios_alloc_custom_align(32, 10 * m * sizeof(int32_t)); - CPLX* dst1 = (CPLX*)(src + 2 * m); - CPLX* dst2 = (CPLX*)(src + 6 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - src[i] = rand() - RAND_MAX / 2; - } - CPLX_FROM_ZNX32_PRECOMP precomp; - precomp.m = m; - cplx_from_znx32_ref(&precomp, dst1, src); - // cplx_from_znx32_simple(m, 32, dst1, src); - cplx_from_znx32_avx2_fma(&precomp, dst2, src); - for (uint64_t i = 0; i < m; ++i) { - ASSERT_EQ(dst1[i][0], dst2[i][0]); - ASSERT_EQ(dst1[i][1], dst2[i][1]); - } - spqlios_free(src); -} -#endif - -#ifdef __x86_64__ -TEST(fft, cplx_from_tnx32_ref_vs_fma) { - const uint32_t m = 128; - int32_t* src = (int32_t*)spqlios_alloc_custom_align(32, 10 * m * sizeof(int32_t)); - CPLX* dst1 = (CPLX*)(src + 2 * m); - CPLX* dst2 = (CPLX*)(src + 6 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - src[i] = rand() + (rand() << 20); - } - CPLX_FROM_TNX32_PRECOMP precomp; - precomp.m = m; - cplx_from_tnx32_ref(&precomp, dst1, src); - // cplx_from_tnx32_simple(m, dst1, src); - cplx_from_tnx32_avx2_fma(&precomp, dst2, src); - for (uint64_t i = 0; i < m; ++i) { - ASSERT_EQ(dst1[i][0], dst2[i][0]); - ASSERT_EQ(dst1[i][1], dst2[i][1]); - } - spqlios_free(src); -} -#endif - -#ifdef __x86_64__ -TEST(fft, cplx_to_tnx32_ref_vs_fma) { - for (const uint32_t m : {8, 128, 1024, 65536}) { - for (const double divisor : {double(1), double(m), double(0.5)}) { - CPLX* src = (CPLX*)spqlios_alloc_custom_align(32, 10 * m * sizeof(int32_t)); - int32_t* dst1 = (int32_t*)(src + m); - int32_t* dst2 = (int32_t*)(src + 2 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - src[i][0] = (rand() / double(RAND_MAX) - 0.5) * pow(2., 19 - (rand() % 60)) * divisor; - src[i][1] = (rand() / double(RAND_MAX) - 0.5) * pow(2., 19 - (rand() % 60)) * divisor; - } - CPLX_TO_TNX32_PRECOMP precomp; - precomp.m = m; - precomp.divisor = divisor; - cplx_to_tnx32_ref(&precomp, dst1, src); - cplx_to_tnx32_avx2_fma(&precomp, dst2, src); - // cplx_to_tnx32_simple(m, divisor, 18, dst2, src); - for (uint64_t i = 0; i < 2 * m; ++i) { - double truevalue = - (src[i % m][i / m] / divisor - floor(src[i % m][i / m] / divisor + 0.5)) * (INT64_C(1) << 32); - if (fabs(truevalue - floor(truevalue)) == 0.5) { - // ties can differ by 0, 1 or -1 - ASSERT_LE(abs(dst1[i] - dst2[i]), 0) - << i << " " << dst1[i] << " " << dst2[i] << " " << truevalue << std::endl; - } else { - // otherwise, we should have equality - ASSERT_LE(abs(dst1[i] - dst2[i]), 0) - << i << " " << dst1[i] << " " << dst2[i] << " " << truevalue << std::endl; - } - } - spqlios_free(src); - } - } -} -#endif
@@ -1,112 +0,0 @@
-#include <benchmark/benchmark.h> -#include <stdint.h> - -#include <cassert> -#include <cmath> -#include <complex> -#include <cstdlib> -#include <cstring> -#include <iostream> -#include <string> -#include <vector> - -#include "../spqlios/cplx/cplx_fft_internal.h" -#include "spqlios/reim/reim_fft.h" - -using namespace std; - -void init_random_values(uint64_t n, double* v) { - for (uint64_t i = 0; i < n; ++i) v[i] = rand() - (RAND_MAX >> 1); -} - -void benchmark_cplx_fft(benchmark::State& state) { - const int32_t nn = state.range(0); - CPLX_FFT_PRECOMP* a = new_cplx_fft_precomp(nn / 2, 1); - double* c = (double*)cplx_fft_precomp_get_buffer(a, 0); - init_random_values(nn, c); - for (auto _ : state) { - // cplx_fft_simple(nn/2, c); - cplx_fft(a, c); - } - delete_cplx_fft_precomp(a); -} - -void benchmark_cplx_ifft(benchmark::State& state) { - const int32_t nn = state.range(0); - CPLX_IFFT_PRECOMP* a = new_cplx_ifft_precomp(nn / 2, 1); - double* c = (double*)cplx_ifft_precomp_get_buffer(a, 0); - init_random_values(nn, c); - for (auto _ : state) { - // cplx_ifft_simple(nn/2, c); - cplx_ifft(a, c); - } - delete_cplx_ifft_precomp(a); -} - -void benchmark_reim_fft(benchmark::State& state) { - const int32_t nn = state.range(0); - const uint32_t m = nn / 2; - REIM_FFT_PRECOMP* a = new_reim_fft_precomp(m, 1); - double* c = reim_fft_precomp_get_buffer(a, 0); - init_random_values(nn, c); - for (auto _ : state) { - // cplx_fft_simple(nn/2, c); - reim_fft(a, c); - } - delete_reim_fft_precomp(a); -} - -#ifdef __aarch64__ -EXPORT REIM_FFT_PRECOMP* new_reim_fft_precomp_neon(uint32_t m, uint32_t num_buffers); -EXPORT void reim_fft_neon(const REIM_FFT_PRECOMP* precomp, double* d); - -void benchmark_reim_fft_neon(benchmark::State& state) { - const int32_t nn = state.range(0); - const uint32_t m = nn / 2; - REIM_FFT_PRECOMP* a = new_reim_fft_precomp_neon(m, 1); - double* c = reim_fft_precomp_get_buffer(a, 0); - init_random_values(nn, c); - for (auto _ : state) { - // cplx_fft_simple(nn/2, c); - reim_fft_neon(a, c); - } - delete_reim_fft_precomp(a); -} -#endif - -void benchmark_reim_ifft(benchmark::State& state) { - const int32_t nn = state.range(0); - const uint32_t m = nn / 2; - REIM_IFFT_PRECOMP* a = new_reim_ifft_precomp(m, 1); - double* c = reim_ifft_precomp_get_buffer(a, 0); - init_random_values(nn, c); - for (auto _ : state) { - // cplx_ifft_simple(nn/2, c); - reim_ifft(a, c); - } - delete_reim_ifft_precomp(a); -} - -// #define ARGS Arg(1024)->Arg(8192)->Arg(32768)->Arg(65536) -#define ARGS Arg(64)->Arg(256)->Arg(1024)->Arg(2048)->Arg(4096)->Arg(8192)->Arg(16384)->Arg(32768)->Arg(65536) - -int main(int argc, char** argv) { - ::benchmark::Initialize(&argc, argv); - if (::benchmark::ReportUnrecognizedArguments(argc, argv)) return 1; - std::cout << "Dimensions n in the benchmark below are in \"real FFT\" modulo X^n+1" << std::endl; - std::cout << "The complex dimension m (modulo X^m-i) is half of it" << std::endl; - BENCHMARK(benchmark_cplx_fft)->ARGS; - BENCHMARK(benchmark_cplx_ifft)->ARGS; - BENCHMARK(benchmark_reim_fft)->ARGS; -#ifdef __aarch64__ - BENCHMARK(benchmark_reim_fft_neon)->ARGS; -#endif - BENCHMARK(benchmark_reim_ifft)->ARGS; - // if (CPU_SUPPORTS("avx512f")) { - // BENCHMARK(bench_cplx_fftvec_twiddle_avx512)->ARGS; - // BENCHMARK(bench_cplx_fftvec_bitwiddle_avx512)->ARGS; - //} - ::benchmark::RunSpecifiedBenchmarks(); - ::benchmark::Shutdown(); - return 0; -}
@@ -1,496 +0,0 @@
-#include <cmath> - -#include "gtest/gtest.h" -#include "spqlios/commons_private.h" -#include "spqlios/cplx/cplx_fft.h" -#include "spqlios/cplx/cplx_fft_internal.h" -#include "spqlios/cplx/cplx_fft_private.h" - -#ifdef __x86_64__ -TEST(fft, ifft16_fma_vs_ref) { - CPLX data[16]; - CPLX omega[8]; - for (uint64_t i = 0; i < 32; ++i) ((double*)data)[i] = 2 * i + 1; //(rand()%100)-50; - for (uint64_t i = 0; i < 16; ++i) ((double*)omega)[i] = i + 1; //(rand()%100)-50; - CPLX copydata[16]; - CPLX copyomega[8]; - memcpy(copydata, data, sizeof(copydata)); - memcpy(copyomega, omega, sizeof(copyomega)); - cplx_ifft16_avx_fma(data, omega); - cplx_ifft16_ref(copydata, copyomega); - double distance = 0; - for (uint64_t i = 0; i < 16; ++i) { - double d1 = fabs(data[i][0] - copydata[i][0]); - double d2 = fabs(data[i][0] - copydata[i][0]); - if (d1 > distance) distance = d1; - if (d2 > distance) distance = d2; - } - /* - printf("data:\n"); - for (uint64_t i=0; i<4; ++i) { - for (uint64_t j=0; j<8; ++j) { - printf("%.5lf ", data[4 * i + j / 2][j % 2]); - } - printf("\n"); - } - printf("copydata:\n"); - for (uint64_t i=0; i<4; ++i) { - for (uint64_t j=0; j<8; ++j) { - printf("%5.5lf ", copydata[4 * i + j / 2][j % 2]); - } - printf("\n"); - } - */ - ASSERT_EQ(distance, 0); -} - -#endif - -void cplx_zero(CPLX r) { r[0] = r[1] = 0; } -void cplx_addmul(CPLX r, const CPLX a, const CPLX b) { - double re = r[0] + a[0] * b[0] - a[1] * b[1]; - double im = r[1] + a[0] * b[1] + a[1] * b[0]; - r[0] = re; - r[1] = im; -} - -void halfcfft_eval(CPLX res, uint32_t nn, uint32_t k, const CPLX* coeffs, const CPLX* powomegas) { - const uint32_t N = nn / 2; - cplx_zero(res); - for (uint64_t i = 0; i < N; ++i) { - cplx_addmul(res, coeffs[i], powomegas[(k * i) % (2 * nn)]); - } -} -void halfcfft_naive(uint32_t nn, CPLX* data) { - const uint32_t N = nn / 2; - CPLX* in = (CPLX*)malloc(N * sizeof(CPLX)); - CPLX* powomega = (CPLX*)malloc(2 * nn * sizeof(CPLX)); - for (uint64_t i = 0; i < (2 * nn); ++i) { - powomega[i][0] = m_accurate_cos((M_PI * i) / nn); - powomega[i][1] = m_accurate_sin((M_PI * i) / nn); - } - memcpy(in, data, N * sizeof(CPLX)); - for (uint64_t j = 0; j < N; ++j) { - uint64_t p = rint(log2(N)) + 2; - uint64_t k = revbits(p, j) + 1; - halfcfft_eval(data[j], nn, k, in, powomega); - } - free(powomega); - free(in); -} - -#ifdef __x86_64__ -TEST(fft, fft16_fma_vs_ref) { - CPLX data[16]; - CPLX omega[8]; - for (uint64_t i = 0; i < 32; ++i) ((double*)data)[i] = rand() % 1000; - for (uint64_t i = 0; i < 16; ++i) ((double*)omega)[i] = rand() % 1000; - CPLX copydata[16]; - CPLX copyomega[8]; - memcpy(copydata, data, sizeof(copydata)); - memcpy(copyomega, omega, sizeof(copyomega)); - cplx_fft16_avx_fma(data, omega); - cplx_fft16_ref(copydata, omega); - double distance = 0; - for (uint64_t i = 0; i < 16; ++i) { - double d1 = fabs(data[i][0] - copydata[i][0]); - double d2 = fabs(data[i][0] - copydata[i][0]); - if (d1 > distance) distance = d1; - if (d2 > distance) distance = d2; - } - ASSERT_EQ(distance, 0); -} -#endif - -TEST(fft, citwiddle_then_invcitwiddle) { - CPLX om; - CPLX ombar; - CPLX data[2]; - CPLX copydata[2]; - om[0] = cos(3); - om[1] = sin(3); - ombar[0] = om[0]; - ombar[1] = -om[1]; - data[0][0] = 47; - data[0][1] = 23; - data[1][0] = -12; - data[1][1] = -9; - memcpy(copydata, data, sizeof(copydata)); - citwiddle(data[0], data[1], om); - invcitwiddle(data[0], data[1], ombar); - double distance = 0; - for (uint64_t i = 0; i < 2; ++i) { - double d1 = fabs(data[i][0] - 2 * copydata[i][0]); - double d2 = fabs(data[i][1] - 2 * copydata[i][1]); - if (d1 > distance) distance = d1; - if (d2 > distance) distance = d2; - } - ASSERT_LE(distance, 1e-9); -} - -TEST(fft, ctwiddle_then_invctwiddle) { - CPLX om; - CPLX ombar; - CPLX data[2]; - CPLX copydata[2]; - om[0] = cos(3); - om[1] = sin(3); - ombar[0] = om[0]; - ombar[1] = -om[1]; - data[0][0] = 47; - data[0][1] = 23; - data[1][0] = -12; - data[1][1] = -9; - memcpy(copydata, data, sizeof(copydata)); - ctwiddle(data[0], data[1], om); - invctwiddle(data[0], data[1], ombar); - double distance = 0; - for (uint64_t i = 0; i < 2; ++i) { - double d1 = fabs(data[i][0] - 2 * copydata[i][0]); - double d2 = fabs(data[i][1] - 2 * copydata[i][1]); - if (d1 > distance) distance = d1; - if (d2 > distance) distance = d2; - } - ASSERT_LE(distance, 1e-9); -} - -TEST(fft, fft16_then_ifft16_ref) { - CPLX full_omegas[64]; - CPLX full_omegabars[64]; - for (uint64_t i = 0; i < 64; ++i) { - full_omegas[i][0] = cos(M_PI * i / 32.); - full_omegas[i][1] = sin(M_PI * i / 32.); - full_omegabars[i][0] = full_omegas[i][0]; - full_omegabars[i][1] = -full_omegas[i][1]; - } - CPLX omega[8]; - CPLX omegabar[8]; - cplx_set(omega[0], full_omegas[8]); // j - cplx_set(omega[1], full_omegas[4]); // k - cplx_set(omega[2], full_omegas[2]); // l - cplx_set(omega[3], full_omegas[10]); // lj - cplx_set(omega[4], full_omegas[1]); // n - cplx_set(omega[5], full_omegas[9]); // nj - cplx_set(omega[6], full_omegas[5]); // nk - cplx_set(omega[7], full_omegas[13]); // njk - cplx_set(omegabar[0], full_omegabars[1]); // n - cplx_set(omegabar[1], full_omegabars[9]); // nj - cplx_set(omegabar[2], full_omegabars[5]); // nk - cplx_set(omegabar[3], full_omegabars[13]); // njk - cplx_set(omegabar[4], full_omegabars[2]); // l - cplx_set(omegabar[5], full_omegabars[10]); // lj - cplx_set(omegabar[6], full_omegabars[4]); // k - cplx_set(omegabar[7], full_omegabars[8]); // j - CPLX data[16]; - CPLX copydata[16]; - for (uint64_t i = 0; i < 32; ++i) ((double*)data)[i] = rand() % 1000; - memcpy(copydata, data, sizeof(copydata)); - cplx_fft16_ref(data, omega); - cplx_ifft16_ref(data, omegabar); - double distance = 0; - for (uint64_t i = 0; i < 16; ++i) { - double d1 = fabs(data[i][0] - 16 * copydata[i][0]); - double d2 = fabs(data[i][0] - 16 * copydata[i][0]); - if (d1 > distance) distance = d1; - if (d2 > distance) distance = d2; - } - ASSERT_LE(distance, 1e-9); -} - -TEST(fft, halfcfft_ref_vs_naive) { - for (uint64_t nn : {4, 8, 16, 64, 256, 8192}) { - uint64_t m = nn / 2; - CPLX_FFT_PRECOMP* tables = new_cplx_fft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, m * sizeof(CPLX)); - CPLX* a1 = (CPLX*)spqlios_alloc_custom_align(32, m * sizeof(CPLX)); - CPLX* a2 = (CPLX*)spqlios_alloc_custom_align(32, m * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < m; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, m * sizeof(CPLX)); - memcpy(a2, a, m * sizeof(CPLX)); - - halfcfft_naive(nn, a1); - cplx_fft_naive(m, 0.25, a2); - - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i][0] - a2[i][0]); - double dim = fabs(a1[i][1] - a2[i][1]); - if (dre > d) d = dre; - if (dim > d) d = dim; - } - ASSERT_LE(d, nn * 1e-10) << nn; - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - delete_cplx_fft_precomp(tables); - } -} - -#ifdef __x86_64__ -TEST(fft, halfcfft_fma_vs_ref) { - typedef void (*FFTF)(const CPLX_FFT_PRECOMP*, void* data); - for (FFTF fft : {cplx_fft_ref, cplx_fft_avx2_fma}) { - for (uint64_t nn : {8, 16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_FFT_PRECOMP* tables = new_cplx_fft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a1 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a2 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - memcpy(a2, a, nn / 2 * sizeof(CPLX)); - cplx_fft_naive(m, 0.25, a2); - fft(tables, a1); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i][0] - a2[i][0]); - double dim = fabs(a1[i][1] - a2[i][1]); - if (dre > d) d = dre; - if (dim > d) d = dim; - } - ASSERT_LE(d, nn * 1e-10) << nn; - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - delete_cplx_fft_precomp(tables); - } - } -} -#endif - -TEST(fft, halfcfft_then_ifft_ref) { - for (uint64_t nn : {4, 8, 16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_FFT_PRECOMP* tables = new_cplx_fft_precomp(m, 0); - CPLX_IFFT_PRECOMP* itables = new_cplx_ifft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a1 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - cplx_fft_ref(tables, a1); - cplx_ifft_ref(itables, a1); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a[i][0] - a1[i][0] / (nn / 2)); - double dim = fabs(a[i][1] - a1[i][1] / (nn / 2)); - if (dre > d) d = dre; - if (dim > d) d = dim; - } - ASSERT_LE(d, 1e-8); - spqlios_free(a); - spqlios_free(a1); - delete_cplx_fft_precomp(tables); - delete_cplx_ifft_precomp(itables); - } -} - -#ifdef __x86_64__ -TEST(fft, halfcfft_ifft_fma_vs_ref) { - for (IFFT_FUNCTION ifft : {cplx_ifft_ref, cplx_ifft_avx2_fma}) { - for (uint64_t nn : {8, 16, 32, 1024, 4096, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_IFFT_PRECOMP* itables = new_cplx_ifft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a1 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a2 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - memcpy(a2, a, nn / 2 * sizeof(CPLX)); - cplx_ifft_naive(m, 0.25, a2); - ifft(itables, a1); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i][0] - a2[i][0]); - double dim = fabs(a1[i][1] - a2[i][1]); - if (dre > d) d = dre; - if (dim > d) d = dim; - } - ASSERT_LE(d, 1e-8); - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - delete_cplx_ifft_precomp(itables); - } - } -} -#endif - -// test the reference and simple implementations of mul on all dimensions -TEST(fftvec, cplx_fftvec_mul_ref) { - for (uint64_t nn : {2, 4, 8, 16, 32, 1024, 4096, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_FFTVEC_MUL_PRECOMP* precomp = new_cplx_fftvec_mul_precomp(m); - CPLX* a = new CPLX[m]; - CPLX* b = new CPLX[m]; - CPLX* r0 = new CPLX[m]; - CPLX* r1 = new CPLX[m]; - CPLX* r2 = new CPLX[m]; - int64_t p = 1 << 16; - for (uint32_t i = 0; i < m; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - b[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - b[i][1] = (rand() % p) - p / 2; - r2[i][0] = r1[i][0] = r0[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - r2[i][1] = r1[i][1] = r0[i][1] = (rand() % p) - p / 2; - } - cplx_fftvec_mul_simple(m, r0, a, b); - cplx_fftvec_mul_ref(precomp, r1, a, b); - for (uint32_t i = 0; i < m; i++) { - r2[i][0] = a[i][0] * b[i][0] - a[i][1] * b[i][1]; - r2[i][1] = a[i][0] * b[i][1] + a[i][1] * b[i][0]; - ASSERT_LE(fabs(r1[i][0] - r2[i][0]) + fabs(r1[i][1] - r2[i][1]), 1e-8); - ASSERT_LE(fabs(r0[i][0] - r2[i][0]) + fabs(r0[i][1] - r2[i][1]), 1e-8); - } - delete[] a; - delete[] b; - delete[] r0; - delete[] r1; - delete[] r2; - delete_cplx_fftvec_mul_precomp(precomp); - } -} - -// test the reference and simple implementations of addmul on all dimensions -TEST(fftvec, cplx_fftvec_addmul_ref) { - for (uint64_t nn : {2, 4, 8, 16, 32, 1024, 4096, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_FFTVEC_ADDMUL_PRECOMP* precomp = new_cplx_fftvec_addmul_precomp(m); - CPLX* a = new CPLX[m]; - CPLX* b = new CPLX[m]; - CPLX* r0 = new CPLX[m]; - CPLX* r1 = new CPLX[m]; - CPLX* r2 = new CPLX[m]; - int64_t p = 1 << 16; - for (uint32_t i = 0; i < m; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - b[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - b[i][1] = (rand() % p) - p / 2; - r2[i][0] = r1[i][0] = r0[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - r2[i][1] = r1[i][1] = r0[i][1] = (rand() % p) - p / 2; - } - cplx_fftvec_addmul_simple(m, r0, a, b); - cplx_fftvec_addmul_ref(precomp, r1, a, b); - for (uint32_t i = 0; i < m; i++) { - r2[i][0] += a[i][0] * b[i][0] - a[i][1] * b[i][1]; - r2[i][1] += a[i][0] * b[i][1] + a[i][1] * b[i][0]; - ASSERT_LE(fabs(r1[i][0] - r2[i][0]) + fabs(r1[i][1] - r2[i][1]), 1e-8); - ASSERT_LE(fabs(r0[i][0] - r2[i][0]) + fabs(r0[i][1] - r2[i][1]), 1e-8); - } - delete[] a; - delete[] b; - delete[] r0; - delete[] r1; - delete[] r2; - delete_cplx_fftvec_addmul_precomp(precomp); - } -} - -// comparative tests between mul ref vs. optimized (only relevant dimensions) -TEST(fftvec, cplx_fftvec_mul_ref_vs_optim) { - struct totest { - FFTVEC_MUL_FUNCTION f; - uint64_t min_m; - totest(FFTVEC_MUL_FUNCTION f, uint64_t min_m) : f(f), min_m(min_m) {} - }; - std::vector<totest> totestset; - totestset.emplace_back(cplx_fftvec_mul, 1); -#ifdef __x86_64__ - totestset.emplace_back(cplx_fftvec_mul_fma, 8); -#endif - for (uint64_t m : {1, 2, 4, 8, 16, 1024, 4096, 8192, 65536}) { - CPLX_FFTVEC_MUL_PRECOMP* precomp = new_cplx_fftvec_mul_precomp(m); - for (const totest& t : totestset) { - if (t.min_m > m) continue; - CPLX* a = new CPLX[m]; - CPLX* b = new CPLX[m]; - CPLX* r1 = new CPLX[m]; - CPLX* r2 = new CPLX[m]; - int64_t p = 1 << 16; - for (uint32_t i = 0; i < m; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - b[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - b[i][1] = (rand() % p) - p / 2; - r2[i][0] = r1[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - r2[i][1] = r1[i][1] = (rand() % p) - p / 2; - } - t.f(precomp, r1, a, b); - cplx_fftvec_mul_ref(precomp, r2, a, b); - for (uint32_t i = 0; i < m; i++) { - double dre = fabs(r1[i][0] - r2[i][0]); - double dim = fabs(r1[i][1] - r2[i][1]); - ASSERT_LE(dre, 1e-8); - ASSERT_LE(dim, 1e-8); - } - delete[] a; - delete[] b; - delete[] r1; - delete[] r2; - } - delete_cplx_fftvec_mul_precomp(precomp); - } -} - -// comparative tests between addmul ref vs. optimized (only relevant dimensions) -TEST(fftvec, cplx_fftvec_addmul_ref_vs_optim) { - struct totest { - FFTVEC_ADDMUL_FUNCTION f; - uint64_t min_m; - totest(FFTVEC_ADDMUL_FUNCTION f, uint64_t min_m) : f(f), min_m(min_m) {} - }; - std::vector<totest> totestset; - totestset.emplace_back(cplx_fftvec_addmul, 1); -#ifdef __x86_64__ - totestset.emplace_back(cplx_fftvec_addmul_fma, 8); -#endif - for (uint64_t m : {1, 2, 4, 8, 16, 1024, 4096, 8192, 65536}) { - CPLX_FFTVEC_ADDMUL_PRECOMP* precomp = new_cplx_fftvec_addmul_precomp(m); - for (const totest& t : totestset) { - if (t.min_m > m) continue; - CPLX* a = new CPLX[m]; - CPLX* b = new CPLX[m]; - CPLX* r1 = new CPLX[m]; - CPLX* r2 = new CPLX[m]; - int64_t p = 1 << 16; - for (uint32_t i = 0; i < m; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - b[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - b[i][1] = (rand() % p) - p / 2; - r2[i][0] = r1[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - r2[i][1] = r1[i][1] = (rand() % p) - p / 2; - } - t.f(precomp, r1, a, b); - cplx_fftvec_addmul_ref(precomp, r2, a, b); - for (uint32_t i = 0; i < m; i++) { - double dre = fabs(r1[i][0] - r2[i][0]); - double dim = fabs(r1[i][1] - r2[i][1]); - ASSERT_LE(dre, 1e-8); - ASSERT_LE(dim, 1e-8); - } - delete[] a; - delete[] b; - delete[] r1; - delete[] r2; - } - delete_cplx_fftvec_addmul_precomp(precomp); - } -}
@@ -1,136 +0,0 @@
-#include <benchmark/benchmark.h> - -#include <cstdint> - -#include "spqlios/q120/q120_arithmetic.h" - -#define ARGS Arg(128)->Arg(4096)->Arg(10000) - -template <typeof(q120_vec_mat1col_product_baa_ref) f> -void benchmark_baa(benchmark::State& state) { - const uint64_t ell = state.range(0); - q120_mat1col_product_baa_precomp* precomp = q120_new_vec_mat1col_product_baa_precomp(); - - uint64_t* a = new uint64_t[ell * 4]; - uint64_t* b = new uint64_t[ell * 4]; - uint64_t* c = new uint64_t[4]; - for (uint64_t i = 0; i < 4 * ell; i++) { - a[i] = rand(); - b[i] = rand(); - } - for (auto _ : state) { - f(precomp, ell, (q120b*)c, (q120a*)a, (q120a*)b); - } - delete[] c; - delete[] b; - delete[] a; - q120_delete_vec_mat1col_product_baa_precomp(precomp); -} - -BENCHMARK(benchmark_baa<q120_vec_mat1col_product_baa_ref>)->Name("q120_vec_mat1col_product_baa_ref")->ARGS; -BENCHMARK(benchmark_baa<q120_vec_mat1col_product_baa_avx2>)->Name("q120_vec_mat1col_product_baa_avx2")->ARGS; - -template <typeof(q120_vec_mat1col_product_bbb_ref) f> -void benchmark_bbb(benchmark::State& state) { - const uint64_t ell = state.range(0); - q120_mat1col_product_bbb_precomp* precomp = q120_new_vec_mat1col_product_bbb_precomp(); - - uint64_t* a = new uint64_t[ell * 4]; - uint64_t* b = new uint64_t[ell * 4]; - uint64_t* c = new uint64_t[4]; - for (uint64_t i = 0; i < 4 * ell; i++) { - a[i] = rand(); - b[i] = rand(); - } - for (auto _ : state) { - f(precomp, ell, (q120b*)c, (q120b*)a, (q120b*)b); - } - delete[] c; - delete[] b; - delete[] a; - q120_delete_vec_mat1col_product_bbb_precomp(precomp); -} - -BENCHMARK(benchmark_bbb<q120_vec_mat1col_product_bbb_ref>)->Name("q120_vec_mat1col_product_bbb_ref")->ARGS; -BENCHMARK(benchmark_bbb<q120_vec_mat1col_product_bbb_avx2>)->Name("q120_vec_mat1col_product_bbb_avx2")->ARGS; - -template <typeof(q120_vec_mat1col_product_bbc_ref) f> -void benchmark_bbc(benchmark::State& state) { - const uint64_t ell = state.range(0); - q120_mat1col_product_bbc_precomp* precomp = q120_new_vec_mat1col_product_bbc_precomp(); - - uint64_t* a = new uint64_t[ell * 4]; - uint64_t* b = new uint64_t[ell * 4]; - uint64_t* c = new uint64_t[4]; - for (uint64_t i = 0; i < 4 * ell; i++) { - a[i] = rand(); - b[i] = rand(); - } - for (auto _ : state) { - f(precomp, ell, (q120b*)c, (q120b*)a, (q120c*)b); - } - delete[] c; - delete[] b; - delete[] a; - q120_delete_vec_mat1col_product_bbc_precomp(precomp); -} - -BENCHMARK(benchmark_bbc<q120_vec_mat1col_product_bbc_ref>)->Name("q120_vec_mat1col_product_bbc_ref")->ARGS; -BENCHMARK(benchmark_bbc<q120_vec_mat1col_product_bbc_avx2>)->Name("q120_vec_mat1col_product_bbc_avx2")->ARGS; - -EXPORT void q120x2_vec_mat2cols_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); -EXPORT void q120x2_vec_mat1col_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); - -template <typeof(q120_vec_mat1col_product_bbc_ref) f> -void benchmark_x2c2_bbc(benchmark::State& state) { - const uint64_t ell = state.range(0); - q120_mat1col_product_bbc_precomp* precomp = q120_new_vec_mat1col_product_bbc_precomp(); - - uint64_t* a = new uint64_t[ell * 8]; - uint64_t* b = new uint64_t[ell * 16]; - uint64_t* c = new uint64_t[16]; - for (uint64_t i = 0; i < 8 * ell; i++) { - a[i] = rand(); - } - for (uint64_t i = 0; i < 16 * ell; i++) { - b[i] = rand(); - } - for (auto _ : state) { - f(precomp, ell, (q120b*)c, (q120b*)a, (q120c*)b); - } - delete[] c; - delete[] b; - delete[] a; - q120_delete_vec_mat1col_product_bbc_precomp(precomp); -} - -BENCHMARK(benchmark_x2c2_bbc<q120x2_vec_mat2cols_product_bbc_avx2>)->Name("q120x2_vec_mat2col_product_bbc_avx2")->ARGS; - -template <typeof(q120_vec_mat1col_product_bbc_ref) f> -void benchmark_x2c1_bbc(benchmark::State& state) { - const uint64_t ell = state.range(0); - q120_mat1col_product_bbc_precomp* precomp = q120_new_vec_mat1col_product_bbc_precomp(); - - uint64_t* a = new uint64_t[ell * 8]; - uint64_t* b = new uint64_t[ell * 8]; - uint64_t* c = new uint64_t[8]; - for (uint64_t i = 0; i < 8 * ell; i++) { - a[i] = rand(); - } - for (uint64_t i = 0; i < 8 * ell; i++) { - b[i] = rand(); - } - for (auto _ : state) { - f(precomp, ell, (q120b*)c, (q120b*)a, (q120c*)b); - } - delete[] c; - delete[] b; - delete[] a; - q120_delete_vec_mat1col_product_bbc_precomp(precomp); -} - -BENCHMARK(benchmark_x2c1_bbc<q120x2_vec_mat1col_product_bbc_avx2>)->Name("q120x2_vec_mat1col_product_bbc_avx2")->ARGS; - -BENCHMARK_MAIN();
@@ -1,437 +0,0 @@
-#include <gtest/gtest.h> - -#include <cstdint> -#include <vector> - -#include "spqlios/q120/q120_arithmetic.h" -#include "test/testlib/negacyclic_polynomial.h" -#include "test/testlib/ntt120_layouts.h" -#include "testlib/mod_q120.h" - -typedef typeof(q120_vec_mat1col_product_baa_ref) vec_mat1col_product_baa_f; - -void test_vec_mat1col_product_baa(vec_mat1col_product_baa_f vec_mat1col_product_baa) { - q120_mat1col_product_baa_precomp* precomp = q120_new_vec_mat1col_product_baa_precomp(); - for (uint64_t ell : {1, 2, 100, 10000}) { - std::vector<uint64_t> a(ell * 4); - std::vector<uint64_t> b(ell * 4); - std::vector<uint64_t> res(4); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = res.data(); - // generate some random data - uniform_q120b(pr); - for (uint64_t i = 0; i < ell; ++i) { - uniform_q120a(pa + 4 * i); - uniform_q120a(pb + 4 * i); - } - // compute the expected result - mod_q120 expect_r; - for (uint64_t i = 0; i < ell; ++i) { - expect_r += mod_q120::from_q120a(pa + 4 * i) * mod_q120::from_q120a(pb + 4 * i); - } - // compute the function - vec_mat1col_product_baa(precomp, ell, (q120b*)pr, (q120a*)pa, (q120a*)pb); - mod_q120 comp_r = mod_q120::from_q120b(pr); - // check for equality - ASSERT_EQ(comp_r, expect_r) << ell; - } - q120_delete_vec_mat1col_product_baa_precomp(precomp); -} - -TEST(q120_arithmetic, q120_vec_mat1col_product_baa_ref) { - test_vec_mat1col_product_baa(q120_vec_mat1col_product_baa_ref); -} -#ifdef __x86_64__ -TEST(q120_arithmetic, q120_vec_mat1col_product_baa_avx2) { - test_vec_mat1col_product_baa(q120_vec_mat1col_product_baa_avx2); -} -#endif - -typedef typeof(q120_vec_mat1col_product_bbb_ref) vec_mat1col_product_bbb_f; - -void test_vec_mat1col_product_bbb(vec_mat1col_product_bbb_f vec_mat1col_product_bbb) { - q120_mat1col_product_bbb_precomp* precomp = q120_new_vec_mat1col_product_bbb_precomp(); - for (uint64_t ell : {1, 2, 100, 10000}) { - std::vector<uint64_t> a(ell * 4); - std::vector<uint64_t> b(ell * 4); - std::vector<uint64_t> res(4); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = res.data(); - // generate some random data - uniform_q120b(pr); - for (uint64_t i = 0; i < ell; ++i) { - uniform_q120b(pa + 4 * i); - uniform_q120b(pb + 4 * i); - } - // compute the expected result - mod_q120 expect_r; - for (uint64_t i = 0; i < ell; ++i) { - expect_r += mod_q120::from_q120b(pa + 4 * i) * mod_q120::from_q120b(pb + 4 * i); - } - // compute the function - vec_mat1col_product_bbb(precomp, ell, (q120b*)pr, (q120b*)pa, (q120b*)pb); - mod_q120 comp_r = mod_q120::from_q120b(pr); - // check for equality - ASSERT_EQ(comp_r, expect_r); - } - q120_delete_vec_mat1col_product_bbb_precomp(precomp); -} - -TEST(q120_arithmetic, q120_vec_mat1col_product_bbb_ref) { - test_vec_mat1col_product_bbb(q120_vec_mat1col_product_bbb_ref); -} -#ifdef __x86_64__ -TEST(q120_arithmetic, q120_vec_mat1col_product_bbb_avx2) { - test_vec_mat1col_product_bbb(q120_vec_mat1col_product_bbb_avx2); -} -#endif - -typedef typeof(q120_vec_mat1col_product_bbc_ref) vec_mat1col_product_bbc_f; - -void test_vec_mat1col_product_bbc(vec_mat1col_product_bbc_f vec_mat1col_product_bbc) { - q120_mat1col_product_bbc_precomp* precomp = q120_new_vec_mat1col_product_bbc_precomp(); - for (uint64_t ell : {1, 2, 100, 10000}) { - std::vector<uint64_t> a(ell * 4); - std::vector<uint64_t> b(ell * 4); - std::vector<uint64_t> res(4); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = res.data(); - // generate some random data - uniform_q120b(pr); - for (uint64_t i = 0; i < ell; ++i) { - uniform_q120b(pa + 4 * i); - uniform_q120c(pb + 4 * i); - } - // compute the expected result - mod_q120 expect_r; - for (uint64_t i = 0; i < ell; ++i) { - expect_r += mod_q120::from_q120b(pa + 4 * i) * mod_q120::from_q120c(pb + 4 * i); - } - // compute the function - vec_mat1col_product_bbc(precomp, ell, (q120b*)pr, (q120b*)pa, (q120c*)pb); - mod_q120 comp_r = mod_q120::from_q120b(pr); - // check for equality - ASSERT_EQ(comp_r, expect_r); - } - q120_delete_vec_mat1col_product_bbc_precomp(precomp); -} - -TEST(q120_arithmetic, q120_vec_mat1col_product_bbc_ref) { - test_vec_mat1col_product_bbc(q120_vec_mat1col_product_bbc_ref); -} -#ifdef __x86_64__ -TEST(q120_arithmetic, q120_vec_mat1col_product_bbc_avx2) { - test_vec_mat1col_product_bbc(q120_vec_mat1col_product_bbc_avx2); -} -#endif - -EXPORT void q120x2_vec_mat2cols_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); -EXPORT void q120x2_vec_mat1col_product_bbc_avx2(q120_mat1col_product_bbc_precomp* precomp, const uint64_t ell, - q120b* const res, const q120b* const x, const q120c* const y); - -typedef typeof(q120x2_vec_mat2cols_product_bbc_avx2) q120x2_prod_bbc_f; - -void test_q120x2_vec_mat2cols_product_bbc(q120x2_prod_bbc_f q120x2_prod_bbc) { - q120_mat1col_product_bbc_precomp* precomp = q120_new_vec_mat1col_product_bbc_precomp(); - for (uint64_t ell : {1, 2, 100, 10000}) { - std::vector<uint64_t> a(ell * 8); - std::vector<uint64_t> b(ell * 16); - std::vector<uint64_t> res(16); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = res.data(); - // generate some random data - uniform_q120b(pr); - for (uint64_t i = 0; i < 2 * ell; ++i) { - uniform_q120b(pa + 4 * i); - } - for (uint64_t i = 0; i < 4 * ell; ++i) { - uniform_q120c(pb + 4 * i); - } - // compute the expected result - mod_q120 expect_r[4]; - for (uint64_t i = 0; i < ell; ++i) { - mod_q120 va = mod_q120::from_q120b(pa + 8 * i); - mod_q120 vb = mod_q120::from_q120b(pa + 8 * i + 4); - mod_q120 m1a = mod_q120::from_q120c(pb + 16 * i); - mod_q120 m1b = mod_q120::from_q120c(pb + 16 * i + 4); - mod_q120 m2a = mod_q120::from_q120c(pb + 16 * i + 8); - mod_q120 m2b = mod_q120::from_q120c(pb + 16 * i + 12); - expect_r[0] += va * m1a; - expect_r[1] += vb * m1b; - expect_r[2] += va * m2a; - expect_r[3] += vb * m2b; - } - // compute the function - q120x2_prod_bbc(precomp, ell, (q120b*)pr, (q120b*)pa, (q120c*)pb); - // check for equality - ASSERT_EQ(mod_q120::from_q120b(pr), expect_r[0]); - ASSERT_EQ(mod_q120::from_q120b(pr + 4), expect_r[1]); - ASSERT_EQ(mod_q120::from_q120b(pr + 8), expect_r[2]); - ASSERT_EQ(mod_q120::from_q120b(pr + 12), expect_r[3]); - } - q120_delete_vec_mat1col_product_bbc_precomp(precomp); -} - -TEST(q120_arithmetic, q120x2_vec_mat2cols_product_bbc_ref) { - test_q120x2_vec_mat2cols_product_bbc(q120x2_vec_mat2cols_product_bbc_ref); -} -#ifdef __x86_64__ -TEST(q120_arithmetic, q120x2_vec_mat2cols_product_bbc_avx2) { - test_q120x2_vec_mat2cols_product_bbc(q120x2_vec_mat2cols_product_bbc_avx2); -} -#endif - -typedef typeof(q120x2_vec_mat1col_product_bbc_avx2) q120x2_c1_prod_bbc_f; - -void test_q120x2_vec_mat1col_product_bbc(q120x2_c1_prod_bbc_f q120x2_c1_prod_bbc) { - q120_mat1col_product_bbc_precomp* precomp = q120_new_vec_mat1col_product_bbc_precomp(); - for (uint64_t ell : {1, 2, 100, 10000}) { - std::vector<uint64_t> a(ell * 8); - std::vector<uint64_t> b(ell * 8); - std::vector<uint64_t> res(8); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = res.data(); - // generate some random data - uniform_q120b(pr); - for (uint64_t i = 0; i < 2 * ell; ++i) { - uniform_q120b(pa + 4 * i); - } - for (uint64_t i = 0; i < 2 * ell; ++i) { - uniform_q120c(pb + 4 * i); - } - // compute the expected result - mod_q120 expect_r[2]; - for (uint64_t i = 0; i < ell; ++i) { - mod_q120 va = mod_q120::from_q120b(pa + 8 * i); - mod_q120 vb = mod_q120::from_q120b(pa + 8 * i + 4); - mod_q120 m1a = mod_q120::from_q120c(pb + 8 * i); - mod_q120 m1b = mod_q120::from_q120c(pb + 8 * i + 4); - expect_r[0] += va * m1a; - expect_r[1] += vb * m1b; - } - // compute the function - q120x2_c1_prod_bbc(precomp, ell, (q120b*)pr, (q120b*)pa, (q120c*)pb); - // check for equality - ASSERT_EQ(mod_q120::from_q120b(pr), expect_r[0]); - ASSERT_EQ(mod_q120::from_q120b(pr + 4), expect_r[1]); - } - q120_delete_vec_mat1col_product_bbc_precomp(precomp); -} - -TEST(q120_arithmetic, q120x2_vec_mat1col_product_bbc_ref) { - test_q120x2_vec_mat1col_product_bbc(q120x2_vec_mat1col_product_bbc_ref); -} -#ifdef __x86_64__ -TEST(q120_arithmetic, q120x2_vec_mat1col_product_bbc_avx2) { - test_q120x2_vec_mat1col_product_bbc(q120x2_vec_mat1col_product_bbc_avx2); -} -#endif - -typedef typeof(q120x2_extract_1blk_from_q120b_ref) q120x2_extract_f; -void test_q120x2_extract_1blk(q120x2_extract_f q120x2_extract) { - for (uint64_t n : {2, 4, 64}) { - ntt120_vec_znx_dft_layout v(n, 1); - std::vector<uint64_t> r(8); - std::vector<uint64_t> expect(8); - for (uint64_t blk = 0; blk < n / 2; ++blk) { - for (uint64_t i = 0; i < 8; ++i) { - expect[i] = uniform_u64(); - } - memcpy(v.get_blk(0, blk), expect.data(), 8 * sizeof(uint64_t)); - q120x2_extract_1blk_from_q120b_ref(n, blk, (q120x2b*)r.data(), (q120b*)v.data); - ASSERT_EQ(r, expect); - } - } -} - -TEST(q120_arithmetic, q120x2_extract_1blk_from_q120b_ref) { - test_q120x2_extract_1blk(q120x2_extract_1blk_from_q120b_ref); -} - -typedef typeof(q120x2_extract_1blk_from_contiguous_q120b_ref) q120x2_extract_vec_f; -void test_q120x2_extract_1blk_vec(q120x2_extract_vec_f q120x2_extract) { - for (uint64_t n : {2, 4, 32}) { - for (uint64_t size : {1, 2, 7}) { - ntt120_vec_znx_dft_layout v(n, size); - std::vector<uint64_t> r(8 * size); - std::vector<uint64_t> expect(8 * size); - for (uint64_t blk = 0; blk < n / 2; ++blk) { - for (uint64_t i = 0; i < 8 * size; ++i) { - expect[i] = uniform_u64(); - } - for (uint64_t i = 0; i < size; ++i) { - memcpy(v.get_blk(i, blk), expect.data() + 8 * i, 8 * sizeof(uint64_t)); - } - q120x2_extract(n, size, blk, (q120x2b*)r.data(), (q120b*)v.data); - ASSERT_EQ(r, expect); - } - } - } -} - -TEST(q120_arithmetic, q120x2_extract_1blk_from_contiguous_q120b_ref) { - test_q120x2_extract_1blk_vec(q120x2_extract_1blk_from_contiguous_q120b_ref); -} - -typedef typeof(q120x2b_save_1blk_to_q120b_ref) q120x2_save_f; -void test_q120x2_save_1blk(q120x2_save_f q120x2_save) { - for (uint64_t n : {2, 4, 64}) { - ntt120_vec_znx_dft_layout v(n, 1); - std::vector<uint64_t> r(8); - std::vector<uint64_t> expect(8); - for (uint64_t blk = 0; blk < n / 2; ++blk) { - for (uint64_t i = 0; i < 8; ++i) { - expect[i] = uniform_u64(); - } - q120x2_save(n, blk, (q120b*)v.data, (q120x2b*)expect.data()); - memcpy(r.data(), v.get_blk(0, blk), 8 * sizeof(uint64_t)); - ASSERT_EQ(r, expect); - } - } -} - -TEST(q120_arithmetic, q120x2b_save_1blk_to_q120b_ref) { test_q120x2_save_1blk(q120x2b_save_1blk_to_q120b_ref); } - -TEST(q120_arithmetic, q120_add_bbb_simple) { - for (const uint64_t n : {2, 4, 1024}) { - std::vector<uint64_t> a(n * 4); - std::vector<uint64_t> b(n * 4); - std::vector<uint64_t> r(n * 4); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = r.data(); - - // generate some random data - for (uint64_t i = 0; i < n; ++i) { - uniform_q120b(pa + 4 * i); - uniform_q120b(pb + 4 * i); - } - - // compute the function - q120_add_bbb_simple(n, (q120b*)pr, (q120b*)pa, (q120b*)pb); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 ae = mod_q120::from_q120b(pa + 4 * i); - mod_q120 be = mod_q120::from_q120b(pb + 4 * i); - mod_q120 re = mod_q120::from_q120b(pr + 4 * i); - - ASSERT_EQ(ae + be, re); - } - } -} - -TEST(q120_arithmetic, q120_add_ccc_simple) { - for (const uint64_t n : {2, 4, 1024}) { - std::vector<uint64_t> a(n * 4); - std::vector<uint64_t> b(n * 4); - std::vector<uint64_t> r(n * 4); - uint64_t* pa = a.data(); - uint64_t* pb = b.data(); - uint64_t* pr = r.data(); - - // generate some random data - for (uint64_t i = 0; i < n; ++i) { - uniform_q120c(pa + 4 * i); - uniform_q120c(pb + 4 * i); - } - - // compute the function - q120_add_ccc_simple(n, (q120c*)pr, (q120c*)pa, (q120c*)pb); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 ae = mod_q120::from_q120c(pa + 4 * i); - mod_q120 be = mod_q120::from_q120c(pb + 4 * i); - mod_q120 re = mod_q120::from_q120c(pr + 4 * i); - - ASSERT_EQ(ae + be, re); - } - } -} - -TEST(q120_arithmetic, q120_c_from_b_simple) { - for (const uint64_t n : {2, 4, 1024}) { - std::vector<uint64_t> a(n * 4); - std::vector<uint64_t> r(n * 4); - uint64_t* pa = a.data(); - uint64_t* pr = r.data(); - - // generate some random data - for (uint64_t i = 0; i < n; ++i) { - uniform_q120b(pa + 4 * i); - } - - // compute the function - q120_c_from_b_simple(n, (q120c*)pr, (q120b*)pa); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 ae = mod_q120::from_q120b(pa + 4 * i); - mod_q120 re = mod_q120::from_q120c(pr + 4 * i); - - ASSERT_EQ(ae, re); - } - } -} - -TEST(q120_arithmetic, q120_b_from_znx64_simple) { - for (const uint64_t n : {2, 4, 1024}) { - znx_i64 x = znx_i64::random_log2bound(n, 62); - std::vector<uint64_t> r(n * 4); - uint64_t* pr = r.data(); - - q120_b_from_znx64_simple(n, (q120b*)pr, x.data()); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 re = mod_q120::from_q120b(pr + 4 * i); - - for (uint64_t k = 0; k < 4; ++k) { - ASSERT_EQ(centermod(x.get_coeff(i), mod_q120::Qi[k]), re.a[k]); - } - } - } -} - -TEST(q120_arithmetic, q120_c_from_znx64_simple) { - for (const uint64_t n : {2, 4, 1024}) { - znx_i64 x = znx_i64::random(n); - std::vector<uint64_t> r(n * 4); - uint64_t* pr = r.data(); - - q120_c_from_znx64_simple(n, (q120c*)pr, x.data()); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 re = mod_q120::from_q120c(pr + 4 * i); - - for (uint64_t k = 0; k < 4; ++k) { - ASSERT_EQ(centermod(x.get_coeff(i), mod_q120::Qi[k]), re.a[k]); - } - } - } -} - -TEST(q120_arithmetic, q120_b_to_znx128_simple) { - for (const uint64_t n : {2, 4, 1024}) { - std::vector<uint64_t> x(n * 4); - uint64_t* px = x.data(); - - // generate some random data - for (uint64_t i = 0; i < n; ++i) { - uniform_q120b(px + 4 * i); - } - - znx_i128 r(n); - q120_b_to_znx128_simple(n, r.data(), (q120b*)px); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 xe = mod_q120::from_q120b(px + 4 * i); - for (uint64_t k = 0; k < 4; ++k) { - ASSERT_EQ(centermod((int64_t)(r.get_coeff(i) % mod_q120::Qi[k]), mod_q120::Qi[k]), xe.a[k]); - } - } - } -}
@@ -1,44 +0,0 @@
-#include <benchmark/benchmark.h> - -#include <cstdint> - -#include "spqlios/q120/q120_ntt.h" - -#define ARGS Arg(1 << 10)->Arg(1 << 11)->Arg(1 << 12)->Arg(1 << 13)->Arg(1 << 14)->Arg(1 << 15)->Arg(1 << 16) - -template <typeof(q120_ntt_bb_avx2) f> -void benchmark_ntt(benchmark::State& state) { - const uint64_t n = state.range(0); - q120_ntt_precomp* precomp = q120_new_ntt_bb_precomp(n); - - uint64_t* px = new uint64_t[n * 4]; - for (uint64_t i = 0; i < 4 * n; i++) { - px[i] = (rand() << 31) + rand(); - } - for (auto _ : state) { - f(precomp, (q120b*)px); - } - delete[] px; - q120_del_ntt_bb_precomp(precomp); -} - -template <typeof(q120_intt_bb_avx2) f> -void benchmark_intt(benchmark::State& state) { - const uint64_t n = state.range(0); - q120_ntt_precomp* precomp = q120_new_intt_bb_precomp(n); - - uint64_t* px = new uint64_t[n * 4]; - for (uint64_t i = 0; i < 4 * n; i++) { - px[i] = (rand() << 31) + rand(); - } - for (auto _ : state) { - f(precomp, (q120b*)px); - } - delete[] px; - q120_del_intt_bb_precomp(precomp); -} - -BENCHMARK(benchmark_ntt<q120_ntt_bb_avx2>)->Name("q120_ntt_bb_avx2")->ARGS; -BENCHMARK(benchmark_intt<q120_intt_bb_avx2>)->Name("q120_intt_bb_avx2")->ARGS; - -BENCHMARK_MAIN();
@@ -1,174 +0,0 @@
-#include <gtest/gtest.h> - -#include <cassert> -#include <cstdint> -#include <cstdio> -#include <random> -#include <vector> - -#include "spqlios/q120/q120_common.h" -#include "spqlios/q120/q120_ntt.h" -#include "testlib/mod_q120.h" - -std::vector<mod_q120> q120_ntt(const std::vector<mod_q120>& x) { - const uint64_t n = x.size(); - - mod_q120 omega_2pow17{OMEGA1, OMEGA2, OMEGA3, OMEGA4}; - mod_q120 omega = pow(omega_2pow17, (1 << 16) / n); - - std::vector<mod_q120> res(n); - for (uint64_t i = 0; i < n; ++i) { - res[i] = x[i]; - } - - for (uint64_t i = 0; i < n; ++i) { - res[i] = res[i] * pow(omega, i); - } - - for (uint64_t nn = n; nn > 1; nn /= 2) { - const uint64_t halfnn = nn / 2; - const uint64_t m = n / halfnn; - - for (uint64_t j = 0; j < n; j += nn) { - for (uint64_t k = 0; k < halfnn; ++k) { - mod_q120 a = res[j + k]; - mod_q120 b = res[j + halfnn + k]; - - res[j + k] = a + b; - res[j + halfnn + k] = (a - b) * pow(omega, k * m); - } - } - } - - return res; -} - -std::vector<mod_q120> q120_intt(const std::vector<mod_q120>& x) { - const uint64_t n = x.size(); - - mod_q120 omega_2pow17{OMEGA1, OMEGA2, OMEGA3, OMEGA4}; - mod_q120 omega = pow(omega_2pow17, (1 << 16) / n); - - std::vector<mod_q120> res(n); - for (uint64_t i = 0; i < n; ++i) { - res[i] = x[i]; - } - - for (uint64_t nn = 2; nn <= n; nn *= 2) { - const uint64_t halfnn = nn / 2; - const uint64_t m = n / halfnn; - - for (uint64_t j = 0; j < n; j += nn) { - for (uint64_t k = 0; k < halfnn; ++k) { - mod_q120 a = res[j + k]; - mod_q120 b = res[j + halfnn + k]; - - mod_q120 bo = b * pow(omega, -k * m); - res[j + k] = a + bo; - res[j + halfnn + k] = a - bo; - } - } - } - - mod_q120 n_q120{(int64_t)n, (int64_t)n, (int64_t)n, (int64_t)n}; - mod_q120 n_inv_q120 = pow(n_q120, -1); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 po = pow(omega, -i) * n_inv_q120; - res[i] = res[i] * po; - } - - return res; -} - -class ntt : public testing::TestWithParam<uint64_t> {}; - -#ifdef __x86_64__ - -TEST_P(ntt, q120_ntt_bb_avx2) { - const uint64_t n = GetParam(); - q120_ntt_precomp* precomp = q120_new_ntt_bb_precomp(n); - - std::vector<uint64_t> x(n * 4); - uint64_t* px = x.data(); - for (uint64_t i = 0; i < 4 * n; i += 4) { - uniform_q120b(px + i); - } - - std::vector<mod_q120> x_modq(n); - for (uint64_t i = 0; i < n; ++i) { - x_modq[i] = mod_q120::from_q120b(px + 4 * i); - } - - std::vector<mod_q120> y_exp = q120_ntt(x_modq); - - q120_ntt_bb_avx2(precomp, (q120b*)px); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 comp_r = mod_q120::from_q120b(px + 4 * i); - ASSERT_EQ(comp_r, y_exp[i]) << i; - } - - q120_del_ntt_bb_precomp(precomp); -} - -TEST_P(ntt, q120_intt_bb_avx2) { - const uint64_t n = GetParam(); - q120_ntt_precomp* precomp = q120_new_intt_bb_precomp(n); - - std::vector<uint64_t> x(n * 4); - uint64_t* px = x.data(); - for (uint64_t i = 0; i < 4 * n; i += 4) { - uniform_q120b(px + i); - } - - std::vector<mod_q120> x_modq(n); - for (uint64_t i = 0; i < n; ++i) { - x_modq[i] = mod_q120::from_q120b(px + 4 * i); - } - - q120_intt_bb_avx2(precomp, (q120b*)px); - - std::vector<mod_q120> y_exp = q120_intt(x_modq); - for (uint64_t i = 0; i < n; ++i) { - mod_q120 comp_r = mod_q120::from_q120b(px + 4 * i); - ASSERT_EQ(comp_r, y_exp[i]) << i; - } - - q120_del_intt_bb_precomp(precomp); -} - -TEST_P(ntt, q120_ntt_intt_bb_avx2) { - const uint64_t n = GetParam(); - q120_ntt_precomp* precomp_ntt = q120_new_ntt_bb_precomp(n); - q120_ntt_precomp* precomp_intt = q120_new_intt_bb_precomp(n); - - std::vector<uint64_t> x(n * 4); - uint64_t* px = x.data(); - for (uint64_t i = 0; i < 4 * n; i += 4) { - uniform_q120b(px + i); - } - - std::vector<mod_q120> x_modq(n); - for (uint64_t i = 0; i < n; ++i) { - x_modq[i] = mod_q120::from_q120b(px + 4 * i); - } - - q120_ntt_bb_avx2(precomp_ntt, (q120b*)px); - q120_intt_bb_avx2(precomp_intt, (q120b*)px); - - for (uint64_t i = 0; i < n; ++i) { - mod_q120 comp_r = mod_q120::from_q120b(px + 4 * i); - ASSERT_EQ(comp_r, x_modq[i]) << i; - } - - q120_del_intt_bb_precomp(precomp_intt); - q120_del_ntt_bb_precomp(precomp_ntt); -} - -INSTANTIATE_TEST_SUITE_P(q120, ntt, - testing::Values(1, 2, 4, 16, 256, UINT64_C(1) << 10, UINT64_C(1) << 11, UINT64_C(1) << 12, - UINT64_C(1) << 13, UINT64_C(1) << 14, UINT64_C(1) << 15, UINT64_C(1) << 16), - testing::PrintToStringParamName()); - -#endif
@@ -1,52 +0,0 @@
-#include <benchmark/benchmark.h> - -#include "spqlios/reim4/reim4_arithmetic.h" - -void init_random_values(uint64_t n, double* v) { - for (uint64_t i = 0; i < n; ++i) - v[i] = (double(rand() % (UINT64_C(1) << 14)) - (UINT64_C(1) << 13)) / (UINT64_C(1) << 12); -} - -// Run the benchmark -BENCHMARK_MAIN(); - -#undef ARGS -#define ARGS Args({47, 16384})->Args({93, 32768}) - -/* - * reim4_vec_mat1col_product - * reim4_vec_mat2col_product - * reim4_vec_mat3col_product - * reim4_vec_mat4col_product - */ - -template <uint64_t X, - void (*fnc)(const uint64_t nrows, double* const dst, const double* const u, const double* const v)> -void benchmark_reim4_vec_matXcols_product(benchmark::State& state) { - const uint64_t nrows = state.range(0); - - double* u = new double[nrows * 8]; - init_random_values(8 * nrows, u); - double* v = new double[nrows * X * 8]; - init_random_values(X * 8 * nrows, v); - double* dst = new double[X * 8]; - - for (auto _ : state) { - fnc(nrows, dst, u, v); - } - - delete[] dst; - delete[] v; - delete[] u; -} - -#undef ARGS -#define ARGS Arg(128)->Arg(1024)->Arg(4096) - -#ifdef __x86_64__ -BENCHMARK(benchmark_reim4_vec_matXcols_product<1, reim4_vec_mat1col_product_avx2>)->ARGS; -// TODO: please remove when fixed: -BENCHMARK(benchmark_reim4_vec_matXcols_product<2, reim4_vec_mat2cols_product_avx2>)->ARGS; -#endif -BENCHMARK(benchmark_reim4_vec_matXcols_product<1, reim4_vec_mat1col_product_ref>)->ARGS; -BENCHMARK(benchmark_reim4_vec_matXcols_product<2, reim4_vec_mat2cols_product_ref>)->ARGS;
@@ -1,291 +0,0 @@
-#include <gtest/gtest.h> - -#include <iostream> -#include <random> - -#include "../spqlios/reim4/reim4_arithmetic.h" -#include "test/testlib/reim4_elem.h" - -/// Actual tests - -typedef typeof(reim4_extract_1blk_from_reim_ref) reim4_extract_1blk_from_reim_f; -void test_reim4_extract_1blk_from_reim(reim4_extract_1blk_from_reim_f reim4_extract_1blk_from_reim) { - static const uint64_t numtrials = 100; - for (uint64_t m : {4, 8, 16, 1024, 4096, 32768}) { - double* v = (double*)malloc(2 * m * sizeof(double)); - double* w = (double*)malloc(8 * sizeof(double)); - reim_view vv(m, v); - for (uint64_t i = 0; i < numtrials; ++i) { - reim4_elem el = gaussian_reim4(); - uint64_t blk = rand() % (m / 4); - vv.set_blk(blk, el); - reim4_extract_1blk_from_reim(m, blk, w, v); - reim4_elem actual(w); - ASSERT_EQ(el, actual); - } - free(v); - free(w); - } -} - -TEST(reim4_arithmetic, reim4_extract_1blk_from_reim_ref) { - test_reim4_extract_1blk_from_reim(reim4_extract_1blk_from_reim_ref); -} -#ifdef __x86_64__ -TEST(reim4_arithmetic, reim4_extract_1blk_from_reim_avx) { - test_reim4_extract_1blk_from_reim(reim4_extract_1blk_from_reim_avx); -} -#endif - -typedef typeof(reim4_save_1blk_to_reim_ref) reim4_save_1blk_to_reim_f; -void test_reim4_save_1blk_to_reim(reim4_save_1blk_to_reim_f reim4_save_1blk_to_reim) { - static const uint64_t numtrials = 100; - for (uint64_t m : {4, 8, 16, 1024, 4096, 32768}) { - double* v = (double*)malloc(2 * m * sizeof(double)); - double* w = (double*)malloc(8 * sizeof(double)); - reim_view vv(m, v); - for (uint64_t i = 0; i < numtrials; ++i) { - reim4_elem el = gaussian_reim4(); - el.save_as(w); - uint64_t blk = rand() % (m / 4); - reim4_save_1blk_to_reim_ref(m, blk, v, w); - reim4_elem actual = vv.get_blk(blk); - ASSERT_EQ(el, actual); - } - free(v); - free(w); - } -} - -TEST(reim4_arithmetic, reim4_save_1blk_to_reim_ref) { test_reim4_save_1blk_to_reim(reim4_save_1blk_to_reim_ref); } -#ifdef __x86_64__ -TEST(reim4_arithmetic, reim4_save_1blk_to_reim_avx) { test_reim4_save_1blk_to_reim(reim4_save_1blk_to_reim_avx); } -#endif - -typedef typeof(reim4_add_1blk_to_reim_ref) reim4_add_1blk_to_reim_f; -void test_reim4_add_1blk_to_reim(reim4_add_1blk_to_reim_f reim4_add_1blk_to_reim) { - static const uint64_t numtrials = 100; - for (uint64_t m : {4, 8, 16, 1024, 4096, 32768}) { - double* v = (double*)malloc(2 * m * sizeof(double)); - double* w1 = (double*)malloc(8 * sizeof(double)); - double* w2 = (double*)malloc(8 * sizeof(double)); - // double* tmp = (double*)malloc(8 * sizeof(double)); - reim_view vv(m, v); - for (uint64_t i = 0; i < numtrials; ++i) { - reim4_elem el1 = gaussian_reim4(); - reim4_elem el2 = gaussian_reim4(); - - // v[blk] = w1 - uint64_t blk = rand() % (m / 4); - el1.save_as(w1); - reim4_save_1blk_to_reim_ref(m, blk, v, w1); - el2.save_as(w2); - - // v[blk] += w2 - reim4_add_1blk_to_reim_ref(m, blk, v, w2); - - reim4_elem w1w2sum = vv.get_blk(blk); - reim4_elem expected_sum = reim4_elem::zero(); - reim4_add(expected_sum.value, w1, w2); - ASSERT_EQ(expected_sum, w1w2sum); - } - free(v); - free(w1); - free(w2); - } -} - -TEST(reim4_arithmetic, reim4_add_1blk_to_reim_ref) { test_reim4_add_1blk_to_reim(reim4_add_1blk_to_reim_ref); } -#ifdef __x86_64__ -TEST(reim4_arithmetic, reim4_add_1blk_to_reim_avx) { test_reim4_add_1blk_to_reim(reim4_add_1blk_to_reim_ref); } -#endif - -typedef typeof(reim4_extract_1blk_from_contiguous_reim_ref) reim4_extract_1blk_from_contiguous_reim_f; -void test_reim4_extract_1blk_from_contiguous_reim( - reim4_extract_1blk_from_contiguous_reim_f reim4_extract_1blk_from_contiguous_reim) { - static const uint64_t numtrials = 20; - for (uint64_t m : {4, 8, 16, 1024, 4096, 32768}) { - for (uint64_t nrows : {1, 2, 5, 128}) { - double* v = (double*)malloc(2 * m * nrows * sizeof(double)); - double* w = (double*)malloc(8 * nrows * sizeof(double)); - reim_vector_view vv(m, nrows, v); - reim4_array_view ww(nrows, w); - for (uint64_t i = 0; i < numtrials; ++i) { - uint64_t blk = rand() % (m / 4); - for (uint64_t j = 0; j < nrows; ++j) { - reim4_elem el = gaussian_reim4(); - vv.row(j).set_blk(blk, el); - } - reim4_extract_1blk_from_contiguous_reim_ref(m, nrows, blk, w, v); - for (uint64_t j = 0; j < nrows; ++j) { - reim4_elem el = vv.row(j).get_blk(blk); - reim4_elem actual = ww.get(j); - ASSERT_EQ(el, actual); - } - } - free(v); - free(w); - } - } -} - -TEST(reim4_arithmetic, reim4_extract_1blk_from_contiguous_reim_ref) { - test_reim4_extract_1blk_from_contiguous_reim(reim4_extract_1blk_from_contiguous_reim_ref); -} -#ifdef __x86_64__ -TEST(reim4_arithmetic, reim4_extract_1blk_from_contiguous_reim_avx) { - test_reim4_extract_1blk_from_contiguous_reim(reim4_extract_1blk_from_contiguous_reim_avx); -} -#endif - -// test of basic arithmetic functions - -TEST(reim4_arithmetic, add) { - reim4_elem x = gaussian_reim4(); - reim4_elem y = gaussian_reim4(); - reim4_elem expect = x + y; - reim4_elem actual; - reim4_add(actual.value, x.value, y.value); - ASSERT_EQ(actual, expect); -} - -TEST(reim4_arithmetic, mul) { - reim4_elem x = gaussian_reim4(); - reim4_elem y = gaussian_reim4(); - reim4_elem expect = x * y; - reim4_elem actual; - reim4_mul(actual.value, x.value, y.value); - ASSERT_EQ(actual, expect); -} - -TEST(reim4_arithmetic, add_mul) { - reim4_elem x = gaussian_reim4(); - reim4_elem y = gaussian_reim4(); - reim4_elem z = gaussian_reim4(); - reim4_elem expect = z; - reim4_elem actual = z; - expect += x * y; - reim4_add_mul(actual.value, x.value, y.value); - ASSERT_EQ(actual, expect) << infty_dist(expect, actual); -} - -// test of dot products - -typedef typeof(reim4_vec_mat1col_product_ref) reim4_vec_mat1col_product_f; -void test_reim4_vec_mat1col_product(reim4_vec_mat1col_product_f product) { - for (uint64_t ell : {1, 2, 5, 13, 69, 129}) { - std::vector<double> actual(8); - std::vector<double> a(ell * 8); - std::vector<double> b(ell * 8); - reim4_array_view va(ell, a.data()); - reim4_array_view vb(ell, b.data()); - reim4_array_view vactual(1, actual.data()); - // initialize random values - for (uint64_t i = 0; i < ell; ++i) { - va.set(i, gaussian_reim4()); - vb.set(i, gaussian_reim4()); - } - // compute the mat1col product - reim4_elem expect; - for (uint64_t i = 0; i < ell; ++i) { - expect += va.get(i) * vb.get(i); - } - // compute the actual product - product(ell, actual.data(), a.data(), b.data()); - // compare - ASSERT_LE(infty_dist(vactual.get(0), expect), 1e-10); - } -} - -TEST(reim4_arithmetic, reim4_vec_mat1col_product_ref) { test_reim4_vec_mat1col_product(reim4_vec_mat1col_product_ref); } -#ifdef __x86_64__ -TEST(reim4_arena, reim4_vec_mat1col_product_avx2) { test_reim4_vec_mat1col_product(reim4_vec_mat1col_product_avx2); } -#endif - -typedef typeof(reim4_vec_mat2cols_product_ref) reim4_vec_mat2col_product_f; -void test_reim4_vec_mat2cols_product(reim4_vec_mat2col_product_f product) { - for (uint64_t ell : {1, 2, 5, 13, 69, 129}) { - std::vector<double> actual(16); - std::vector<double> a(ell * 8); - std::vector<double> b(ell * 16); - reim4_array_view va(ell, a.data()); - reim4_matrix_view vb(ell, 2, b.data()); - reim4_array_view vactual(2, actual.data()); - // initialize random values - for (uint64_t i = 0; i < ell; ++i) { - va.set(i, gaussian_reim4()); - vb.set(i, 0, gaussian_reim4()); - vb.set(i, 1, gaussian_reim4()); - } - // compute the mat1col product - reim4_elem expect[2]; - for (uint64_t i = 0; i < ell; ++i) { - expect[0] += va.get(i) * vb.get(i, 0); - expect[1] += va.get(i) * vb.get(i, 1); - } - // compute the actual product - product(ell, actual.data(), a.data(), b.data()); - // compare - ASSERT_LE(infty_dist(vactual.get(0), expect[0]), 1e-10); - ASSERT_LE(infty_dist(vactual.get(1), expect[1]), 1e-10); - } -} - -TEST(reim4_arithmetic, reim4_vec_mat2cols_product_ref) { - test_reim4_vec_mat2cols_product(reim4_vec_mat2cols_product_ref); -} -#ifdef __x86_64__ -TEST(reim4_arithmetic, reim4_vec_mat2cols_product_avx2) { - test_reim4_vec_mat2cols_product(reim4_vec_mat2cols_product_avx2); -} -#endif - -// for now, we do not need avx implementations, -// so we will keep a single test function -TEST(reim4_arithmetic, reim4_vec_convolution_ref) { - for (uint64_t sizea : {1, 2, 3, 5, 8}) { - for (uint64_t sizeb : {1, 3, 6, 9, 13}) { - std::vector<double> a(8 * sizea); - std::vector<double> b(8 * sizeb); - std::vector<double> expect(8 * (sizea + sizeb - 1)); - std::vector<double> actual(8 * (sizea + sizeb - 1)); - reim4_array_view va(sizea, a.data()); - reim4_array_view vb(sizeb, b.data()); - std::vector<reim4_elem> vexpect(sizea + sizeb + 3); - reim4_array_view vactual(sizea + sizeb - 1, actual.data()); - for (uint64_t i = 0; i < sizea; ++i) { - va.set(i, gaussian_reim4()); - } - for (uint64_t j = 0; j < sizeb; ++j) { - vb.set(j, gaussian_reim4()); - } - // manual convolution - for (uint64_t i = 0; i < sizea; ++i) { - for (uint64_t j = 0; j < sizeb; ++j) { - vexpect[i + j] += va.get(i) * vb.get(j); - } - } - // partial convolution single coeff - for (uint64_t k = 0; k < sizea + sizeb + 3; ++k) { - double dest[8] = {0}; - reim4_convolution_1coeff_ref(k, dest, a.data(), sizea, b.data(), sizeb); - ASSERT_LE(infty_dist(reim4_elem(dest), vexpect[k]), 1e-10); - } - // partial convolution dual coeff - for (uint64_t k = 0; k < sizea + sizeb + 2; ++k) { - double dest[16] = {0}; - reim4_convolution_2coeff_ref(k, dest, a.data(), sizea, b.data(), sizeb); - ASSERT_LE(infty_dist(reim4_elem(dest), vexpect[k]), 1e-10); - ASSERT_LE(infty_dist(reim4_elem(dest + 8), vexpect[k + 1]), 1e-10); - } - // actual convolution - reim4_convolution_ref(actual.data(), sizea + sizeb - 1, 0, a.data(), sizea, b.data(), sizeb); - for (uint64_t k = 0; k < sizea + sizeb - 1; ++k) { - ASSERT_LE(infty_dist(vactual.get(k), vexpect[k]), 1e-10) << k; - } - } - } -} - -EXPORT void reim4_convolution_ref(double* dest, uint64_t dest_size, uint64_t dest_offset, const double* a, - uint64_t sizea, const double* b, uint64_t sizeb);
@@ -1,115 +0,0 @@
-#include <gtest/gtest.h> -#include <spqlios/reim/reim_fft_internal.h> - -#include "testlib/test_commons.h" - -TEST(reim_conversions, reim_to_tnx) { - for (uint32_t m : {1, 2, 64, 128, 512}) { - for (double divisor : {1, 2, int(m)}) { - for (uint32_t log2overhead : {1, 2, 10, 18, 35, 42}) { - double maxdiff = pow(2., log2overhead - 50); - std::vector<double> data(2 * m); - std::vector<double> dout(2 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - data[i] = (uniform_f64_01() - 0.5) * pow(2., log2overhead + 1) * divisor; - } - REIM_TO_TNX_PRECOMP* p = new_reim_to_tnx_precomp(m, divisor, 18); - reim_to_tnx_ref(p, dout.data(), data.data()); - for (uint64_t i = 0; i < 2 * m; ++i) { - ASSERT_LE(fabs(dout[i]), 0.5); - double diff = dout[i] - data[i] / divisor; - double fracdiff = diff - rint(diff); - ASSERT_LE(fabs(fracdiff), maxdiff); - } - delete_reim_to_tnx_precomp(p); - } - } - } -} - -#ifdef __x86_64__ -TEST(reim_conversions, reim_to_tnx_ref_vs_avx) { - for (uint32_t m : {8, 16, 64, 128, 512}) { - for (double divisor : {1, 2, int(m)}) { - for (uint32_t log2overhead : {1, 2, 10, 18, 35, 42}) { - // double maxdiff = pow(2., log2overhead - 50); - std::vector<double> data(2 * m); - std::vector<double> dout1(2 * m); - std::vector<double> dout2(2 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - data[i] = (uniform_f64_01() - 0.5) * pow(2., log2overhead + 1) * divisor; - } - REIM_TO_TNX_PRECOMP* p = new_reim_to_tnx_precomp(m, divisor, 18); - reim_to_tnx_ref(p, dout1.data(), data.data()); - reim_to_tnx_avx(p, dout2.data(), data.data()); - for (uint64_t i = 0; i < 2 * m; ++i) { - ASSERT_LE(fabs(dout1[i] - dout2[i]), 0.); - } - delete_reim_to_tnx_precomp(p); - } - } - } -} -#endif - -typedef typeof(reim_from_znx64_ref) reim_from_znx64_f; - -void test_reim_from_znx64(reim_from_znx64_f reim_from_znx64, uint64_t maxbnd) { - for (uint32_t m : {4, 8, 16, 64, 16384}) { - REIM_FROM_ZNX64_PRECOMP* p = new_reim_from_znx64_precomp(m, maxbnd); - std::vector<int64_t> data(2 * m); - std::vector<double> dout(2 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - int64_t magnitude = int64_t(uniform_u64() % (maxbnd + 1)); - data[i] = uniform_i64() >> (63 - magnitude); - REQUIRE_DRAMATICALLY(abs(data[i]) <= (INT64_C(1) << magnitude), "pb"); - } - reim_from_znx64(p, dout.data(), data.data()); - for (uint64_t i = 0; i < 2 * m; ++i) { - ASSERT_EQ(dout[i], double(data[i])) << dout[i] << " " << data[i]; - } - delete_reim_from_znx64_precomp(p); - } -} - -TEST(reim_conversions, reim_from_znx64) { - for (uint64_t maxbnd : {50}) { - test_reim_from_znx64(reim_from_znx64, maxbnd); - } -} -TEST(reim_conversions, reim_from_znx64_ref) { test_reim_from_znx64(reim_from_znx64_ref, 50); } -#ifdef __x86_64__ -TEST(reim_conversions, reim_from_znx64_avx2_bnd50_fma) { test_reim_from_znx64(reim_from_znx64_bnd50_fma, 50); } -#endif - -typedef typeof(reim_to_znx64_ref) reim_to_znx64_f; - -void test_reim_to_znx64(reim_to_znx64_f reim_to_znx64_fcn, int64_t maxbnd) { - for (uint32_t m : {4, 8, 16, 64, 16384}) { - for (double divisor : {1, 2, int(m)}) { - REIM_TO_ZNX64_PRECOMP* p = new_reim_to_znx64_precomp(m, divisor, maxbnd); - std::vector<double> data(2 * m); - std::vector<int64_t> dout(2 * m); - for (uint64_t i = 0; i < 2 * m; ++i) { - int64_t magnitude = int64_t(uniform_u64() % (maxbnd + 11)) - 10; - data[i] = (uniform_f64_01() - 0.5) * pow(2., magnitude + 1) * divisor; - } - reim_to_znx64_fcn(p, dout.data(), data.data()); - for (uint64_t i = 0; i < 2 * m; ++i) { - ASSERT_LE(dout[i] - data[i] / divisor, 0.5) << dout[i] << " " << data[i]; - } - delete_reim_to_znx64_precomp(p); - } - } -} - -TEST(reim_conversions, reim_to_znx64) { - for (uint64_t maxbnd : {63, 50}) { - test_reim_to_znx64(reim_to_znx64, maxbnd); - } -} -TEST(reim_conversions, reim_to_znx64_ref) { test_reim_to_znx64(reim_to_znx64_ref, 63); } -#ifdef __x86_64__ -TEST(reim_conversions, reim_to_znx64_avx2_bnd63_fma) { test_reim_to_znx64(reim_to_znx64_avx2_bnd63_fma, 63); } -TEST(reim_conversions, reim_to_znx64_avx2_bnd50_fma) { test_reim_to_znx64(reim_to_znx64_avx2_bnd50_fma, 50); } -#endif
@@ -1,513 +0,0 @@
-#include <inttypes.h> - -#include <cmath> - -#include "gtest/gtest.h" -#include "spqlios/commons_private.h" -#include "spqlios/cplx/cplx_fft_internal.h" -#include "spqlios/reim/reim_fft_internal.h" -#include "spqlios/reim/reim_fft_private.h" - -#ifdef __x86_64__ -TEST(fft, reim_fft_avx2_vs_fft_reim_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - // CPLX_FFT_PRECOMP* tables = new_cplx_fft_precomp(m, 0); - REIM_FFT_PRECOMP* reimtables = new_reim_fft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - memcpy(a2, a, nn / 2 * sizeof(CPLX)); - reim_fft_ref(reimtables, a2); - reim_fft_avx2_fma(reimtables, a1); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i] - a2[i]); - double dim = fabs(a1[nn / 2 + i] - a2[nn / 2 + i]); - if (dre > d) d = dre; - if (dim > d) d = dim; - ASSERT_LE(d, nn * 1e-10) << nn; - } - ASSERT_LE(d, nn * 1e-10) << nn; - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - // delete_cplx_fft_precomp(tables); - delete_reim_fft_precomp(reimtables); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim_ifft_avx2_vs_reim_ifft_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - // CPLX_FFT_PRECOMP* tables = new_cplx_fft_precomp(m, 0); - REIM_IFFT_PRECOMP* reimtables = new_reim_ifft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - memcpy(a2, a, nn / 2 * sizeof(CPLX)); - reim_ifft_ref(reimtables, a2); - reim_ifft_avx2_fma(reimtables, a1); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i] - a2[i]); - double dim = fabs(a1[nn / 2 + i] - a2[nn / 2 + i]); - if (dre > d) d = dre; - if (dim > d) d = dim; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - // delete_cplx_fft_precomp(tables); - delete_reim_fft_precomp(reimtables); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim_vecfft_addmul_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM_FFTVEC_ADDMUL_PRECOMP* tbl = new_reim_fftvec_addmul_precomp(m); - ASSERT_TRUE(tbl != nullptr); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - b1[i] = (rand() % p) - p / 2; - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(b2, b1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim_fftvec_addmul_ref(tbl, r1, a1, b1); - reim_fftvec_addmul_fma(tbl, r2, a2, b2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(b1); - spqlios_free(b2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim_fftvec_addmul_precomp(tbl); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim_vecfft_mul_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM_FFTVEC_MUL_PRECOMP* tbl = new_reim_fftvec_mul_precomp(m); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - b1[i] = (rand() % p) - p / 2; - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(b2, b1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim_fftvec_mul_ref(tbl, r1, a1, b1); - reim_fftvec_mul_fma(tbl, r2, a2, b2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(b1); - spqlios_free(b2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim_fftvec_mul_precomp(tbl); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim_vecfft_add_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM_FFTVEC_ADD_PRECOMP* tbl = new_reim_fftvec_add_precomp(m); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - b1[i] = (rand() % p) - p / 2; - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(b2, b1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim_fftvec_add_ref(tbl, r1, a1, b1); - reim_fftvec_add_fma(tbl, r2, a2, b2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(b1); - spqlios_free(b2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim_fftvec_add_precomp(tbl); - } -} -#endif - -typedef void (*FILL_REIM_FFT_OMG_F)(const double entry_pwr, double** omg); -typedef void (*REIM_FFT_F)(double* dre, double* dim, const void* omega); - -// template to test a fixed-dimension fft vs. naive -template <uint64_t N> -void test_reim_fft_ref_vs_naive(FILL_REIM_FFT_OMG_F fill_omega_f, REIM_FFT_F reim_fft_f) { - double om[N]; - double data[2 * N]; - double datacopy[2 * N]; - double* omg = om; - fill_omega_f(0.25, &omg); - ASSERT_EQ(omg - om, ptrdiff_t(N)); // it may depend on N - for (uint64_t i = 0; i < N; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[N + i] = data[N + i] = (rand() % 100) - 50; - } - reim_fft_f(datacopy, datacopy + N, om); - reim_naive_fft(N, 0.25, data, data + N); - double d = 0; - for (uint64_t i = 0; i < 2 * N; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-7); -} - -template <uint64_t N> -void test_reim_fft_ref_vs_accel(REIM_FFT_F reim_fft_ref_f, REIM_FFT_F reim_fft_accel_f) { - double om[N]; - double data[2 * N]; - double datacopy[2 * N]; - for (uint64_t i = 0; i < N; ++i) { - om[i] = (rand() % 100) - 50; - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[N + i] = data[N + i] = (rand() % 100) - 50; - } - reim_fft_ref_f(datacopy, datacopy + N, om); - reim_fft_accel_f(data, data + N, om); - double d = 0; - for (uint64_t i = 0; i < 2 * N; ++i) { - d += fabs(datacopy[i] - data[i]); - } - if (d > 1e-15) { - for (uint64_t i = 0; i < N; ++i) { - printf("%" PRId64 " %lf %lf %lf %lf\n", i, data[i], data[N + i], datacopy[i], datacopy[N + i]); - } - ASSERT_LE(d, 0); - } -} - -TEST(fft, reim_fft16_ref_vs_naive) { test_reim_fft_ref_vs_naive<16>(fill_reim_fft16_omegas, reim_fft16_ref); } -#ifdef __aarch64__ -TEST(fft, reim_fft16_neon_vs_naive) { test_reim_fft_ref_vs_naive<16>(fill_reim_fft16_omegas_neon, reim_fft16_neon); } -#endif - -#ifdef __x86_64__ -TEST(fft, reim_fft16_ref_vs_fma) { test_reim_fft_ref_vs_accel<16>(reim_fft16_ref, reim_fft16_avx_fma); } -#endif - -#ifdef __aarch64__ -static void reim_fft16_ref_neon_pom(double* dre, double* dim, const void* omega) { - const double* pom = (double*)omega; - // put the omegas in neon order - double x_pom[] = {pom[0], pom[1], pom[2], pom[3], pom[4], pom[5], pom[6], pom[7], - pom[8], pom[10], pom[12], pom[14], pom[9], pom[11], pom[13], pom[15]}; - reim_fft16_ref(dre, dim, x_pom); -} -TEST(fft, reim_fft16_ref_vs_neon) { test_reim_fft_ref_vs_accel<16>(reim_fft16_ref_neon_pom, reim_fft16_neon); } -#endif - -TEST(fft, reim_fft8_ref_vs_naive) { test_reim_fft_ref_vs_naive<8>(fill_reim_fft8_omegas, reim_fft8_ref); } - -#ifdef __x86_64__ -TEST(fft, reim_fft8_ref_vs_fma) { test_reim_fft_ref_vs_accel<8>(reim_fft8_ref, reim_fft8_avx_fma); } -#endif - -TEST(fft, reim_fft4_ref_vs_naive) { test_reim_fft_ref_vs_naive<4>(fill_reim_fft4_omegas, reim_fft4_ref); } - -#ifdef __x86_64__ -TEST(fft, reim_fft4_ref_vs_fma) { test_reim_fft_ref_vs_accel<4>(reim_fft4_ref, reim_fft4_avx_fma); } -#endif - -TEST(fft, reim_fft2_ref_vs_naive) { test_reim_fft_ref_vs_naive<2>(fill_reim_fft2_omegas, reim_fft2_ref); } - -TEST(fft, reim_fft_bfs_16_ref_vs_naive) { - for (const uint64_t m : {16, 32, 64, 128, 256, 512, 1024, 2048}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - double* omg = om.data(); - fill_reim_fft_bfs_16_omegas(m, 0.25, &omg); - ASSERT_LE(omg - om.data(), ptrdiff_t(2 * m)); // it may depend on m - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - omg = om.data(); - reim_fft_bfs_16_ref(m, datacopy.data(), datacopy.data() + m, &omg); - reim_naive_fft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-7); - } -} - -TEST(fft, reim_fft_rec_16_ref_vs_naive) { - for (const uint64_t m : {2048, 4096, 8192, 32768, 65536}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - double* omg = om.data(); - fill_reim_fft_rec_16_omegas(m, 0.25, &omg); - ASSERT_LE(omg - om.data(), ptrdiff_t(2 * m)); // it may depend on m - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - omg = om.data(); - reim_fft_rec_16_ref(m, datacopy.data(), datacopy.data() + m, &omg); - reim_naive_fft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-5); - } -} - -TEST(fft, reim_fft_ref_vs_naive) { - for (const uint64_t m : {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 32768, 65536}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - REIM_FFT_PRECOMP* precomp = new_reim_fft_precomp(m, 0); - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - reim_fft_ref(precomp, datacopy.data()); - reim_naive_fft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-5) << m; - delete_reim_fft_precomp(precomp); - } -} - -#ifdef __aarch64__ -EXPORT REIM_FFT_PRECOMP* new_reim_fft_precomp_neon(uint32_t m, uint32_t num_buffers); -EXPORT void reim_fft_neon(const REIM_FFT_PRECOMP* precomp, double* d); -TEST(fft, reim_fft_neon_vs_naive) { - for (const uint64_t m : {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 32768, 65536}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - REIM_FFT_PRECOMP* precomp = new_reim_fft_precomp_neon(m, 0); - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - reim_fft_neon(precomp, datacopy.data()); - reim_naive_fft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-5) << m; - delete_reim_fft_precomp(precomp); - } -} -#endif - -typedef void (*FILL_REIM_IFFT_OMG_F)(const double entry_pwr, double** omg); -typedef void (*REIM_IFFT_F)(double* dre, double* dim, const void* omega); - -// template to test a fixed-dimension fft vs. naive -template <uint64_t N> -void test_reim_ifft_ref_vs_naive(FILL_REIM_IFFT_OMG_F fill_omega_f, REIM_IFFT_F reim_ifft_f) { - double om[N]; - double data[2 * N]; - double datacopy[2 * N]; - double* omg = om; - fill_omega_f(0.25, &omg); - ASSERT_EQ(omg - om, ptrdiff_t(N)); // it may depend on N - for (uint64_t i = 0; i < N; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[N + i] = data[N + i] = (rand() % 100) - 50; - } - reim_ifft_f(datacopy, datacopy + N, om); - reim_naive_ifft(N, 0.25, data, data + N); - double d = 0; - for (uint64_t i = 0; i < 2 * N; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-7); -} - -template <uint64_t N> -void test_reim_ifft_ref_vs_accel(REIM_IFFT_F reim_ifft_ref_f, REIM_IFFT_F reim_ifft_accel_f) { - double om[N]; - double data[2 * N]; - double datacopy[2 * N]; - for (uint64_t i = 0; i < N; ++i) { - om[i] = (rand() % 100) - 50; - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[N + i] = data[N + i] = (rand() % 100) - 50; - } - reim_ifft_ref_f(datacopy, datacopy + N, om); - reim_ifft_accel_f(data, data + N, om); - double d = 0; - for (uint64_t i = 0; i < 2 * N; ++i) { - d += fabs(datacopy[i] - data[i]); - } - if (d > 1e-15) { - for (uint64_t i = 0; i < N; ++i) { - printf("%" PRId64 " %lf %lf %lf %lf\n", i, data[i], data[N + i], datacopy[i], datacopy[N + i]); - } - ASSERT_LE(d, 0); - } -} - -TEST(fft, reim_ifft16_ref_vs_naive) { test_reim_ifft_ref_vs_naive<16>(fill_reim_ifft16_omegas, reim_ifft16_ref); } - -#ifdef __x86_64__ -TEST(fft, reim_ifft16_ref_vs_fma) { test_reim_ifft_ref_vs_accel<16>(reim_ifft16_ref, reim_ifft16_avx_fma); } -#endif - -TEST(fft, reim_ifft8_ref_vs_naive) { test_reim_ifft_ref_vs_naive<8>(fill_reim_ifft8_omegas, reim_ifft8_ref); } - -#ifdef __x86_64__ -TEST(fft, reim_ifft8_ref_vs_fma) { test_reim_ifft_ref_vs_accel<8>(reim_ifft8_ref, reim_ifft8_avx_fma); } -#endif - -TEST(fft, reim_ifft4_ref_vs_naive) { test_reim_ifft_ref_vs_naive<4>(fill_reim_ifft4_omegas, reim_ifft4_ref); } - -#ifdef __x86_64__ -TEST(fft, reim_ifft4_ref_vs_fma) { test_reim_ifft_ref_vs_accel<4>(reim_ifft4_ref, reim_ifft4_avx_fma); } -#endif - -TEST(fft, reim_ifft2_ref_vs_naive) { test_reim_ifft_ref_vs_naive<2>(fill_reim_ifft2_omegas, reim_ifft2_ref); } - -TEST(fft, reim_ifft_bfs_16_ref_vs_naive) { - for (const uint64_t m : {16, 32, 64, 128, 256, 512, 1024, 2048}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - double* omg = om.data(); - fill_reim_ifft_bfs_16_omegas(m, 0.25, &omg); - ASSERT_LE(omg - om.data(), ptrdiff_t(2 * m)); // it may depend on m - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - omg = om.data(); - reim_ifft_bfs_16_ref(m, datacopy.data(), datacopy.data() + m, &omg); - reim_naive_ifft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-7); - } -} - -TEST(fft, reim_ifft_rec_16_ref_vs_naive) { - for (const uint64_t m : {2048, 4096, 8192, 32768, 65536}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - double* omg = om.data(); - fill_reim_ifft_rec_16_omegas(m, 0.25, &omg); - ASSERT_LE(omg - om.data(), ptrdiff_t(2 * m)); // it may depend on m - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - omg = om.data(); - reim_ifft_rec_16_ref(m, datacopy.data(), datacopy.data() + m, &omg); - reim_naive_ifft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-5); - } -} - -TEST(fft, reim_ifft_ref_vs_naive) { - for (const uint64_t m : {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 32768, 65536}) { - std::vector<double> om(2 * m); - std::vector<double> data(2 * m); - std::vector<double> datacopy(2 * m); - REIM_IFFT_PRECOMP* precomp = new_reim_ifft_precomp(m, 0); - for (uint64_t i = 0; i < m; ++i) { - datacopy[i] = data[i] = (rand() % 100) - 50; - datacopy[m + i] = data[m + i] = (rand() % 100) - 50; - } - reim_ifft_ref(precomp, datacopy.data()); - reim_naive_ifft(m, 0.25, data.data(), data.data() + m); - double d = 0; - for (uint64_t i = 0; i < 2 * m; ++i) { - d += fabs(datacopy[i] - data[i]); - } - ASSERT_LE(d, 1e-5) << m; - delete_reim_ifft_precomp(precomp); - } -}
@@ -1,47 +0,0 @@
-#include <gtest/gtest.h> - -#include "../spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "testlib/fft64_dft.h" -#include "testlib/fft64_layouts.h" -#include "testlib/polynomial_vector.h" - -void test_fft64_svp_apply_dft(SVP_APPLY_DFT_F svp) { - for (uint64_t n : {2, 4, 8, 64, 128}) { - MODULE* module = new_module_info(n, FFT64); - // poly 1 to multiply - create and prepare - fft64_svp_ppol_layout ppol(n); - ppol.fill_random(1.); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - uint64_t a_sl = n + uniform_u64_bits(2); - // poly 2 to multiply - znx_vec_i64_layout a(n, sa, a_sl); - a.fill_random(19); - // original operation result - fft64_vec_znx_dft_layout res(n, sr); - thash hash_a_before = a.content_hash(); - thash hash_ppol_before = ppol.content_hash(); - svp(module, res.data, sr, ppol.data, a.data(), sa, a_sl); - ASSERT_EQ(a.content_hash(), hash_a_before); - ASSERT_EQ(ppol.content_hash(), hash_ppol_before); - // create expected value - reim_fft64vec ppo = ppol.get_copy(); - std::vector<reim_fft64vec> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - expect[i] = ppo * simple_fft64(a.get_copy_zext(i)); - } - // this is the largest precision we can safely expect - double prec_expect = n * pow(2., 19 - 52); - for (uint64_t i = 0; i < sr; ++i) { - reim_fft64vec actual = res.get_copy_zext(i); - ASSERT_LE(infty_dist(actual, expect[i]), prec_expect); - } - } - } - - delete_module_info(module); - } -} - -TEST(fft64_svp_apply_dft, svp_apply_dft) { test_fft64_svp_apply_dft(svp_apply_dft); } -TEST(fft64_svp_apply_dft, fft64_svp_apply_dft_ref) { test_fft64_svp_apply_dft(fft64_svp_apply_dft_ref); }
@@ -1,28 +0,0 @@
-#include <gtest/gtest.h> - -#include "../spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "testlib/fft64_dft.h" -#include "testlib/fft64_layouts.h" -#include "testlib/polynomial_vector.h" - -// todo: remove when registered -typedef typeof(fft64_svp_prepare_ref) SVP_PREPARE_F; - -void test_fft64_svp_prepare(SVP_PREPARE_F svp_prepare) { - for (uint64_t n : {2, 4, 8, 64, 128}) { - MODULE* module = new_module_info(n, FFT64); - znx_i64 in = znx_i64::random_log2bound(n, 40); - fft64_svp_ppol_layout out(n); - reim_fft64vec expect = simple_fft64(in); - svp_prepare(module, out.data, in.data()); - const double* ed = (double*)expect.data(); - const double* ac = (double*)out.data; - for (uint64_t i = 0; i < n; ++i) { - ASSERT_LE(abs(ed[i] - ac[i]), 1e-10) << i << n; - } - delete_module_info(module); - } -} - -TEST(svp_prepare, fft64_svp_prepare_ref) { test_fft64_svp_prepare(fft64_svp_prepare_ref); } -TEST(svp_prepare, svp_prepare) { test_fft64_svp_prepare(svp_prepare); }
@@ -1,493 +0,0 @@
-#include <cassert> -#include <cinttypes> -#include <cmath> -#include <complex> -#include <cstdint> -#include <cstdlib> -#include <iostream> -#include <string> -#include <vector> - -#include "gtest/gtest.h" -#include "spqlios/cplx/cplx_fft_internal.h" - -using namespace std; - -/*namespace { -bool very_close(const double& a, const double& b) { - bool reps = (abs(a - b) < 1e-5); - if (!reps) { - cerr << "not close: " << a << " vs. " << b << endl; - } - return reps; -} - -}*/ // namespace - -TEST(fft, fftvec_convolution) { - uint64_t nn = 65536; // vary accross (8192, 16384), 32768, 65536 - static const uint64_t k = 18; // vary from 10 to 20 - // double* buf_fft = fft_precomp_get_buffer(tables, 0); - // double* buf_ifft = ifft_precomp_get_buffer(itables, 0); - double* a = (double*)spqlios_alloc_custom_align(32, nn * 8); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn * 8); - double* b = (double*)spqlios_alloc_custom_align(32, nn * 8); - double* dist_vector = (double*)spqlios_alloc_custom_align(32, nn * 8); - int64_t p = UINT64_C(1) << k; - printf("p size: %" PRId64 "\n", p); - for (uint32_t i = 0; i < nn; i++) { - a[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - b[i] = (rand() % p) - p / 2; - a2[i] = 0; - } - cplx_fft_simple(nn / 2, a); - cplx_fft_simple(nn / 2, b); - cplx_fftvec_addmul_simple(nn / 2, a2, a, b); - cplx_ifft_simple(nn / 2, a2); // normalization is missing - double distance = 0; - // for (int32_t i = 0; i < 10; i++) { - // printf("%lf %lf\n", a2[i], a2[i] / (nn / 2.)); - //} - for (uint32_t i = 0; i < nn; i++) { - double curdist = fabs(a2[i] / (nn / 2.) - rint(a2[i] / (nn / 2.))); - if (distance < curdist) distance = curdist; - dist_vector[i] = a2[i] / (nn / 2.) - rint(a2[i] / (nn / 2.)); - } - printf("distance: %lf\n", distance); - ASSERT_LE(distance, 0.5); // switch from previous 0.1 to 0.5 per experiment 1 reqs - // double a3[] = {2,4,4,4,5,5,7,9}; //instead of dist_vector, for test - // nn = 8; - double mean = 0; - for (uint32_t i = 0; i < nn; i++) { - mean = mean + dist_vector[i]; - } - mean = mean / nn; - double variance = 0; - for (uint32_t i = 0; i < nn; i++) { - variance = variance + pow((mean - dist_vector[i]), 2); - } - double stdev = sqrt(variance / nn); - printf("stdev: %lf\n", stdev); - - spqlios_free(a); - spqlios_free(b); - spqlios_free(a2); - spqlios_free(dist_vector); -} - -typedef double CPLX[2]; -EXPORT uint32_t revbits(uint32_t i, uint32_t v); - -void cplx_zero(CPLX r); -void cplx_addmul(CPLX r, const CPLX a, const CPLX b); - -void halfcfft_eval(CPLX res, uint32_t nn, uint32_t k, const CPLX* coeffs, const CPLX* powomegas); -void halfcfft_naive(uint32_t nn, CPLX* data); - -EXPORT void cplx_set(CPLX, const CPLX); -EXPORT void citwiddle(CPLX, CPLX, const CPLX); -EXPORT void invcitwiddle(CPLX, CPLX, const CPLX); -EXPORT void ctwiddle(CPLX, CPLX, const CPLX); -EXPORT void invctwiddle(CPLX, CPLX, const CPLX); - -#include "../spqlios/cplx/cplx_fft_private.h" -#include "../spqlios/reim/reim_fft_internal.h" -#include "../spqlios/reim/reim_fft_private.h" -#include "../spqlios/reim4/reim4_fftvec_internal.h" -#include "../spqlios/reim4/reim4_fftvec_private.h" - -TEST(fft, simple_fft_test) { // test for checking the simple_fft api - uint64_t nn = 8; // vary accross (8192, 16384), 32768, 65536 - // double* buf_fft = fft_precomp_get_buffer(tables, 0); - // double* buf_ifft = ifft_precomp_get_buffer(itables, 0); - - // define the complex coefficients of two polynomials mod X^4-i - double a[4][2] = {{1.1, 2.2}, {3.3, 4.4}, {5.5, 6.6}, {7.7, 8.8}}; - double b[4][2] = {{9., 10.}, {11., 12.}, {13., 14.}, {15., 16.}}; - double c[4][2]; // for the result - double a2[4][2]; // for testing inverse fft - memcpy(a2, a, 8 * nn); - cplx_fft_simple(4, a); - cplx_fft_simple(4, b); - cplx_fftvec_mul_simple(4, c, a, b); - cplx_ifft_simple(4, c); - // c contains the complex coefficients 4.a*b mod X^4-i - cplx_ifft_simple(4, a); - - double distance = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dist = fabs(a[i][0] / 4. - a2[i][0]); - if (distance < dist) distance = dist; - dist = fabs(a[i][1] / 4. - a2[i][1]); - if (distance < dist) distance = dist; - } - printf("distance: %lf\n", distance); - ASSERT_LE(distance, 0.1); // switch from previous 0.1 to 0.5 per experiment 1 reqs - - for (uint32_t i = 0; i < nn / 4; i++) { - printf("%lf %lf\n", a2[i][0], a[i][0] / (nn / 2.)); - printf("%lf %lf\n", a2[i][1], a[i][1] / (nn / 2.)); - } -} - -TEST(fft, reim_test) { - // double a[16] __attribute__ ((aligned(32)))= {1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9,10.,11.,12.,13.,14.,15.,16.}; - // double b[16] __attribute__ ((aligned(32)))= {17.,18.,19.,20.,21.,22.,23.,24.,25.,26.,27.,28.,29.,30., 31.,32.}; - // double c[16] __attribute__ ((aligned(32))); // for the result in reference layout - double a[16] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 10., 11., 12., 13., 14., 15., 16.}; - double b[16] = {17., 18., 19., 20., 21., 22., 23., 24., 25., 26., 27., 28., 29., 30., 31., 32.}; - double c[16]; // for the result in reference layout - reim_fft_simple(8, a); - reim_fft_simple(8, b); - reim_fftvec_mul_simple(8, c, a, b); - reim_ifft_simple(8, c); -} - -TEST(fft, reim_vs_regular_layout_mul_test) { - uint64_t nn = 16; - - // define the complex coefficients of two polynomials mod X^8-i - - double a1[8][2] __attribute__((aligned(32))) = {{1.1, 2.2}, {3.3, 4.4}, {5.5, 6.6}, {7.7, 8.8}, - {9.9, 10.}, {11., 12.}, {13., 14.}, {15., 16.}}; - double b1[8][2] __attribute__((aligned(32))) = {{17., 18.}, {19., 20.}, {21., 22.}, {23., 24.}, - {25., 26.}, {27., 28.}, {29., 30.}, {31., 32.}}; - double c1[8][2] __attribute__((aligned(32))); // for the result - double c2[16] __attribute__((aligned(32))); // for the result - double c3[8][2] __attribute__((aligned(32))); // for the result - - double* a2 = - (double*)spqlios_alloc_custom_align(32, nn / 2 * 2 * sizeof(double)); // for storing the coefs in reim layout - double* b2 = - (double*)spqlios_alloc_custom_align(32, nn / 2 * 2 * sizeof(double)); // for storing the coefs in reim layout - // double* c2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); // for storing the coefs in reim - // layout - - // organise the coefficients in the reim layout - for (uint32_t i = 0; i < nn / 2; i++) { - a2[i] = a1[i][0]; // a1 = a2, b1 = b2 - a2[nn / 2 + i] = a1[i][1]; - b2[i] = b1[i][0]; - b2[nn / 2 + i] = b1[i][1]; - } - - // fft - cplx_fft_simple(8, a1); - reim_fft_simple(8, a2); - - cplx_fft_simple(8, b1); - reim_fft_simple(8, b2); - - cplx_fftvec_mul_simple(8, c1, a1, b1); - reim_fftvec_mul_simple(8, c2, a2, b2); - - cplx_ifft_simple(8, c1); - reim_ifft_simple(8, c2); - - // check base layout and reim layout result in the same values - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - // printf("RE: cplx_result %lf and reim_result %lf \n", c1[i][0], c2[i]); - // printf("IM: cplx_result %lf and reim_result %lf \n", c1[i][1], c2[nn / 2 + i]); - double dre = fabs(c1[i][0] - c2[i]); - double dim = fabs(c1[i][1] - c2[nn / 2 + i]); - if (dre > d) d = dre; - if (dim > d) d = dim; - ASSERT_LE(d, 1e-7); - } - ASSERT_LE(d, 1e-7); - - // check converting back to base layout: - - for (uint32_t i = 0; i < nn / 2; i++) { - c3[i][0] = c2[i]; - c3[i][1] = c2[8 + i]; - } - - d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(c1[i][0] - c3[i][0]); - double dim = fabs(c1[i][1] - c3[i][1]); - if (dre > d) d = dre; - if (dim > d) d = dim; - ASSERT_LE(d, 1e-7); - } - ASSERT_LE(d, 1e-7); - - spqlios_free(a2); - spqlios_free(b2); - // spqlios_free(c2); -} - -TEST(fft, fftvec_convolution_recursiveoverk) { - static const uint64_t nn = 32768; // vary accross (8192, 16384), 32768, 65536 - double* a = (double*)spqlios_alloc_custom_align(32, nn * 8); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn * 8); - double* b = (double*)spqlios_alloc_custom_align(32, nn * 8); - double* dist_vector = (double*)spqlios_alloc_custom_align(32, nn * 8); - - printf("N size: %" PRId64 "\n", nn); - - for (uint32_t k = 14; k <= 24; k++) { // vary k - printf("k size: %" PRId32 "\n", k); - int64_t p = UINT64_C(1) << k; - for (uint32_t i = 0; i < nn; i++) { - a[i] = (rand() % p) - p / 2; - b[i] = (rand() % p) - p / 2; - a2[i] = 0; - } - cplx_fft_simple(nn / 2, a); - cplx_fft_simple(nn / 2, b); - cplx_fftvec_addmul_simple(nn / 2, a2, a, b); - cplx_ifft_simple(nn / 2, a2); - double distance = 0; - for (uint32_t i = 0; i < nn; i++) { - double curdist = fabs(a2[i] / (nn / 2.) - rint(a2[i] / (nn / 2.))); - if (distance < curdist) distance = curdist; - dist_vector[i] = a2[i] / (nn / 2.) - rint(a2[i] / (nn / 2.)); - } - printf("distance: %lf\n", distance); - ASSERT_LE(distance, 0.5); // switch from previous 0.1 to 0.5 per experiment 1 reqs - double mean = 0; - for (uint32_t i = 0; i < nn; i++) { - mean = mean + dist_vector[i]; - } - mean = mean / nn; - double variance = 0; - for (uint32_t i = 0; i < nn; i++) { - variance = variance + pow((mean - dist_vector[i]), 2); - } - double stdev = sqrt(variance / nn); - printf("stdev: %lf\n", stdev); - } - - spqlios_free(a); - spqlios_free(b); - spqlios_free(a2); - spqlios_free(dist_vector); -} - -#ifdef __x86_64__ -TEST(fft, cplx_fft_ref_vs_fft_reim_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_FFT_PRECOMP* tables = new_cplx_fft_precomp(m, 0); - REIM_FFT_PRECOMP* reimtables = new_reim_fft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a1 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - for (uint32_t i = 0; i < nn / 2; i++) { - a2[i] = a[i][0]; - a2[nn / 2 + i] = a[i][1]; - } - cplx_fft_ref(tables, a1); - reim_fft_ref(reimtables, a2); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i][0] - a2[i]); - double dim = fabs(a1[i][1] - a2[nn / 2 + i]); - if (dre > d) d = dre; - if (dim > d) d = dim; - ASSERT_LE(d, 1e-7); - } - ASSERT_LE(d, 1e-7); - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - delete_cplx_fft_precomp(tables); - delete_reim_fft_precomp(reimtables); - } -} -#endif - -TEST(fft, cplx_ifft_ref_vs_reim_ifft_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - CPLX_IFFT_PRECOMP* tables = new_cplx_ifft_precomp(m, 0); - REIM_IFFT_PRECOMP* reimtables = new_reim_ifft_precomp(m, 0); - CPLX* a = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - CPLX* a1 = (CPLX*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn / 2; i++) { - a[i][0] = (rand() % p) - p / 2; // between -p/2 and p/2 - a[i][1] = (rand() % p) - p / 2; - } - memcpy(a1, a, nn / 2 * sizeof(CPLX)); - for (uint32_t i = 0; i < nn / 2; i++) { - a2[i] = a[i][0]; - a2[nn / 2 + i] = a[i][1]; - } - cplx_ifft_ref(tables, a1); - reim_ifft_ref(reimtables, a2); - double d = 0; - for (uint32_t i = 0; i < nn / 2; i++) { - double dre = fabs(a1[i][0] - a2[i]); - double dim = fabs(a1[i][1] - a2[nn / 2 + i]); - if (dre > d) d = dre; - if (dim > d) d = dim; - ASSERT_LE(d, 1e-7); - } - ASSERT_LE(d, 1e-7); - spqlios_free(a); - spqlios_free(a1); - spqlios_free(a2); - delete_cplx_fft_precomp(tables); - delete_reim_fft_precomp(reimtables); - } -} - -#ifdef __x86_64__ -TEST(fft, reim4_vecfft_addmul_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM4_FFTVEC_ADDMUL_PRECOMP* tbl = new_reim4_fftvec_addmul_precomp(m); - ASSERT_TRUE(tbl != nullptr); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - b1[i] = (rand() % p) - p / 2; - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(b2, b1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim4_fftvec_addmul_ref(tbl, r1, a1, b1); - reim4_fftvec_addmul_fma(tbl, r2, a2, b2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(b1); - spqlios_free(b2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim4_fftvec_addmul_precomp(tbl); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim4_vecfft_mul_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM4_FFTVEC_MUL_PRECOMP* tbl = new_reim4_fftvec_mul_precomp(m); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* b2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - b1[i] = (rand() % p) - p / 2; - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(b2, b1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim4_fftvec_mul_ref(tbl, r1, a1, b1); - reim4_fftvec_mul_fma(tbl, r2, a2, b2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(b1); - spqlios_free(b2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim_fftvec_mul_precomp(tbl); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim4_from_cplx_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM4_FROM_CPLX_PRECOMP* tbl = new_reim4_from_cplx_precomp(m); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim4_from_cplx_ref(tbl, r1, a1); - reim4_from_cplx_fma(tbl, r2, a2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim4_from_cplx_precomp(tbl); - } -} -#endif - -#ifdef __x86_64__ -TEST(fft, reim4_to_cplx_fma_vs_ref) { - for (uint64_t nn : {16, 32, 64, 1024, 8192, 65536}) { - uint64_t m = nn / 2; - REIM4_TO_CPLX_PRECOMP* tbl = new_reim4_to_cplx_precomp(m); - double* a1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* a2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r1 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - double* r2 = (double*)spqlios_alloc_custom_align(32, nn / 2 * sizeof(CPLX)); - int64_t p = 1 << 16; - for (uint32_t i = 0; i < nn; i++) { - a1[i] = (rand() % p) - p / 2; // between -p/2 and p/2 - r1[i] = (rand() % p) - p / 2; - } - memcpy(a2, a1, nn / 2 * sizeof(CPLX)); - memcpy(r2, r1, nn / 2 * sizeof(CPLX)); - reim4_to_cplx_ref(tbl, r1, a1); - reim4_to_cplx_fma(tbl, r2, a2); - double d = 0; - for (uint32_t i = 0; i < nn; i++) { - double di = fabs(r1[i] - r2[i]); - if (di > d) d = di; - ASSERT_LE(d, 1e-8); - } - ASSERT_LE(d, 1e-8); - spqlios_free(a1); - spqlios_free(a2); - spqlios_free(r1); - spqlios_free(r2); - delete_reim4_from_cplx_precomp(tbl); - } -} -#endif
@@ -1,42 +0,0 @@
-#include "gtest/gtest.h" -#include "spqlios/arithmetic/vec_rnx_arithmetic_private.h" -#include "testlib/vec_rnx_layout.h" - -static void test_rnx_approxdecomp(RNX_APPROXDECOMP_FROM_TNXDBL_F approxdec) { - for (const uint64_t nn : {2, 4, 8, 32}) { - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (const uint64_t ell : {1, 2, 7}) { - for (const uint64_t k : {2, 5}) { - TNXDBL_APPROXDECOMP_GADGET* gadget = new_tnxdbl_approxdecomp_gadget(module, k, ell); - for (const uint64_t res_size : {ell, ell - 1, ell + 1}) { - const uint64_t res_sl = nn + uniform_u64_bits(2); - rnx_vec_f64_layout in(nn, 1, nn); - in.fill_random(3); - rnx_vec_f64_layout out(nn, res_size, res_sl); - approxdec(module, gadget, out.data(), res_size, res_sl, in.data()); - // reconstruct the output - uint64_t msize = std::min(res_size, ell); - double err_bnd = msize == ell ? pow(2., -double(msize * k) - 1) : pow(2., -double(msize * k)); - for (uint64_t j = 0; j < nn; ++j) { - double in_j = in.data()[j]; - double out_j = 0; - for (uint64_t i = 0; i < res_size; ++i) { - out_j += out.get_copy(i).get_coeff(j) * pow(2., -double((i + 1) * k)); - } - double err = out_j - in_j; - double err_abs = fabs(err - rint(err)); - ASSERT_LE(err_abs, err_bnd); - } - } - delete_tnxdbl_approxdecomp_gadget(gadget); - } - } - delete_rnx_module_info(module); - } -} - -TEST(vec_rnx, rnx_approxdecomp) { test_rnx_approxdecomp(rnx_approxdecomp_from_tnxdbl); } -TEST(vec_rnx, rnx_approxdecomp_ref) { test_rnx_approxdecomp(rnx_approxdecomp_from_tnxdbl_ref); } -#ifdef __x86_64__ -TEST(vec_rnx, rnx_approxdecomp_avx) { test_rnx_approxdecomp(rnx_approxdecomp_from_tnxdbl_avx); } -#endif
@@ -1,134 +0,0 @@
-#include <gtest/gtest.h> -#include <spqlios/arithmetic/vec_rnx_arithmetic_private.h> - -#include "testlib/test_commons.h" - -template <typename SRC_T, typename DST_T> -static void test_conv(void (*conv_f)(const MOD_RNX*, // - DST_T* res, uint64_t res_size, uint64_t res_sl, // - const SRC_T* a, uint64_t a_size, uint64_t a_sl), // - DST_T (*ideal_conv_f)(SRC_T x), // - SRC_T (*random_f)() // -) { - for (uint64_t nn : {2, 4, 16, 64}) { - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t a_size : {0, 1, 2, 5}) { - for (uint64_t res_size : {0, 1, 3, 5}) { - for (uint64_t trials = 0; trials < 20; ++trials) { - uint64_t a_sl = nn + uniform_u64_bits(2); - uint64_t res_sl = nn + uniform_u64_bits(2); - std::vector<SRC_T> a(a_sl * a_size); - std::vector<DST_T> res(res_sl * res_size); - uint64_t msize = std::min(a_size, res_size); - for (uint64_t i = 0; i < a_size; ++i) { - for (uint64_t j = 0; j < nn; ++j) { - a[i * a_sl + j] = random_f(); - } - } - conv_f(module, res.data(), res_size, res_sl, a.data(), a_size, a_sl); - for (uint64_t i = 0; i < msize; ++i) { - for (uint64_t j = 0; j < nn; ++j) { - SRC_T aij = a[i * a_sl + j]; - DST_T expect = ideal_conv_f(aij); - DST_T actual = res[i * res_sl + j]; - ASSERT_EQ(expect, actual); - } - } - for (uint64_t i = msize; i < res_size; ++i) { - DST_T expect = 0; - for (uint64_t j = 0; j < nn; ++j) { - SRC_T actual = res[i * res_sl + j]; - ASSERT_EQ(expect, actual); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -static int32_t ideal_dbl_to_tn32(double a) { - double _2p32 = INT64_C(1) << 32; - double a_mod_1 = a - rint(a); - int64_t t = rint(a_mod_1 * _2p32); - return int32_t(t); -} - -static double random_f64_10() { return uniform_f64_bounds(-10, 10); } - -static void test_vec_rnx_to_tnx32(VEC_RNX_TO_TNX32_F vec_rnx_to_tnx32_f) { - test_conv(vec_rnx_to_tnx32_f, ideal_dbl_to_tn32, random_f64_10); -} - -TEST(vec_rnx_arithmetic, vec_rnx_to_tnx32) { test_vec_rnx_to_tnx32(vec_rnx_to_tnx32); } -TEST(vec_rnx_arithmetic, vec_rnx_to_tnx32_ref) { test_vec_rnx_to_tnx32(vec_rnx_to_tnx32_ref); } - -static double ideal_tn32_to_dbl(int32_t a) { - const double _2p32 = INT64_C(1) << 32; - return double(a) / _2p32; -} - -static int32_t random_t32() { return uniform_i64_bits(32); } - -static void test_vec_rnx_from_tnx32(VEC_RNX_FROM_TNX32_F vec_rnx_from_tnx32_f) { - test_conv(vec_rnx_from_tnx32_f, ideal_tn32_to_dbl, random_t32); -} - -TEST(vec_rnx_arithmetic, vec_rnx_from_tnx32) { test_vec_rnx_from_tnx32(vec_rnx_from_tnx32); } -TEST(vec_rnx_arithmetic, vec_rnx_from_tnx32_ref) { test_vec_rnx_from_tnx32(vec_rnx_from_tnx32_ref); } - -static int32_t ideal_dbl_round_to_i32(double a) { return int32_t(rint(a)); } - -static double random_dbl_explaw_18() { return uniform_f64_bounds(-1., 1.) * pow(2., uniform_u64_bits(6) % 19); } - -static void test_vec_rnx_to_znx32(VEC_RNX_TO_ZNX32_F vec_rnx_to_znx32_f) { - test_conv(vec_rnx_to_znx32_f, ideal_dbl_round_to_i32, random_dbl_explaw_18); -} - -TEST(zn_arithmetic, vec_rnx_to_znx32) { test_vec_rnx_to_znx32(vec_rnx_to_znx32); } -TEST(zn_arithmetic, vec_rnx_to_znx32_ref) { test_vec_rnx_to_znx32(vec_rnx_to_znx32_ref); } - -static double ideal_i32_to_dbl(int32_t a) { return double(a); } - -static int32_t random_i32_explaw_18() { return uniform_i64_bits(uniform_u64_bits(6) % 19); } - -static void test_vec_rnx_from_znx32(VEC_RNX_FROM_ZNX32_F vec_rnx_from_znx32_f) { - test_conv(vec_rnx_from_znx32_f, ideal_i32_to_dbl, random_i32_explaw_18); -} - -TEST(zn_arithmetic, vec_rnx_from_znx32) { test_vec_rnx_from_znx32(vec_rnx_from_znx32); } -TEST(zn_arithmetic, vec_rnx_from_znx32_ref) { test_vec_rnx_from_znx32(vec_rnx_from_znx32_ref); } - -static double ideal_dbl_to_tndbl(double a) { return a - rint(a); } - -static void test_vec_rnx_to_tnxdbl(VEC_RNX_TO_TNXDBL_F vec_rnx_to_tnxdbl_f) { - test_conv(vec_rnx_to_tnxdbl_f, ideal_dbl_to_tndbl, random_f64_10); -} - -TEST(zn_arithmetic, vec_rnx_to_tnxdbl) { test_vec_rnx_to_tnxdbl(vec_rnx_to_tnxdbl); } -TEST(zn_arithmetic, vec_rnx_to_tnxdbl_ref) { test_vec_rnx_to_tnxdbl(vec_rnx_to_tnxdbl_ref); } - -#if 0 -static int64_t ideal_dbl_round_to_i64(double a) { return rint(a); } - -static double random_dbl_explaw_50() { return uniform_f64_bounds(-1., 1.) * pow(2., uniform_u64_bits(7) % 51); } - -static void test_dbl_round_to_i64(DBL_ROUND_TO_I64_F dbl_round_to_i64_f) { - test_conv(dbl_round_to_i64_f, ideal_dbl_round_to_i64, random_dbl_explaw_50); -} - -TEST(zn_arithmetic, dbl_round_to_i64) { test_dbl_round_to_i64(dbl_round_to_i64); } -TEST(zn_arithmetic, dbl_round_to_i64_ref) { test_dbl_round_to_i64(dbl_round_to_i64_ref); } - -static double ideal_i64_to_dbl(int64_t a) { return double(a); } - -static int64_t random_i64_explaw_50() { return uniform_i64_bits(uniform_u64_bits(7) % 51); } - -static void test_i64_to_dbl(I64_TO_DBL_F i64_to_dbl_f) { - test_conv(i64_to_dbl_f, ideal_i64_to_dbl, random_i64_explaw_50); -} - -TEST(zn_arithmetic, i64_to_dbl) { test_i64_to_dbl(i64_to_dbl); } -TEST(zn_arithmetic, i64_to_dbl_ref) { test_i64_to_dbl(i64_to_dbl_ref); } -#endif
@@ -1,73 +0,0 @@
-#include <gtest/gtest.h> - -#include "spqlios/arithmetic/vec_rnx_arithmetic_private.h" -#include "spqlios/reim/reim_fft.h" -#include "test/testlib/vec_rnx_layout.h" - -static void test_vec_rnx_svp_prepare(RNX_SVP_PREPARE_F* rnx_svp_prepare, BYTES_OF_RNX_SVP_PPOL_F* tmp_bytes) { - for (uint64_t n : {2, 4, 8, 64}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - const double invm = 1. / mod->m; - - rnx_f64 in = rnx_f64::random_log2bound(n, 40); - rnx_f64 in_divide_by_m = rnx_f64::zero(n); - for (uint64_t i = 0; i < n; ++i) { - in_divide_by_m.set_coeff(i, in.get_coeff(i) * invm); - } - fft64_rnx_svp_ppol_layout out(n); - reim_fft64vec expect = simple_fft64(in_divide_by_m); - rnx_svp_prepare(mod, out.data, in.data()); - const double* ed = (double*)expect.data(); - const double* ac = (double*)out.data; - for (uint64_t i = 0; i < n; ++i) { - ASSERT_LE(abs(ed[i] - ac[i]), 1e-10) << i << n; - } - delete_rnx_module_info(mod); - } -} -TEST(vec_rnx, vec_rnx_svp_prepare) { test_vec_rnx_svp_prepare(rnx_svp_prepare, bytes_of_rnx_svp_ppol); } -TEST(vec_rnx, vec_rnx_svp_prepare_ref) { - test_vec_rnx_svp_prepare(fft64_rnx_svp_prepare_ref, fft64_bytes_of_rnx_svp_ppol); -} - -static void test_vec_rnx_svp_apply(RNX_SVP_APPLY_F* apply) { - for (uint64_t n : {2, 4, 8, 64, 128}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - - // poly 1 to multiply - create and prepare - fft64_rnx_svp_ppol_layout ppol(n); - ppol.fill_random(1.); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - uint64_t a_sl = n + uniform_u64_bits(2); - uint64_t r_sl = n + uniform_u64_bits(2); - // poly 2 to multiply - rnx_vec_f64_layout a(n, sa, a_sl); - a.fill_random(19); - - // original operation result - rnx_vec_f64_layout res(n, sr, r_sl); - thash hash_a_before = a.content_hash(); - thash hash_ppol_before = ppol.content_hash(); - apply(mod, res.data(), sr, r_sl, ppol.data, a.data(), sa, a_sl); - ASSERT_EQ(a.content_hash(), hash_a_before); - ASSERT_EQ(ppol.content_hash(), hash_ppol_before); - // create expected value - reim_fft64vec ppo = ppol.get_copy(); - std::vector<rnx_f64> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - expect[i] = simple_ifft64(ppo * simple_fft64(a.get_copy_zext(i))); - } - // this is the largest precision we can safely expect - double prec_expect = n * pow(2., 19 - 50); - for (uint64_t i = 0; i < sr; ++i) { - rnx_f64 actual = res.get_copy_zext(i); - ASSERT_LE(infty_dist(actual, expect[i]), prec_expect); - } - } - } - delete_rnx_module_info(mod); - } -} -TEST(vec_rnx, vec_rnx_svp_apply) { test_vec_rnx_svp_apply(rnx_svp_apply); } -TEST(vec_rnx, vec_rnx_svp_apply_ref) { test_vec_rnx_svp_apply(fft64_rnx_svp_apply_ref); }
@@ -1,417 +0,0 @@
-#include <gtest/gtest.h> - -#include "spqlios/arithmetic/vec_rnx_arithmetic_private.h" -#include "spqlios/reim/reim_fft.h" -#include "testlib/vec_rnx_layout.h" - -// disabling this test by default, since it depicts on purpose wrong accesses -#if 0 -TEST(rnx_layout, valgrind_antipattern_test) { - uint64_t n = 4; - rnx_vec_f64_layout v(n, 7, 13); - // this should be ok - v.set(0, rnx_f64::zero(n)); - // this should abort (wrong ring dimension) - ASSERT_DEATH(v.set(3, rnx_f64::zero(2 * n)), ""); - // this should abort (out of bounds) - ASSERT_DEATH(v.set(8, rnx_f64::zero(n)), ""); - // this should be ok - ASSERT_EQ(v.get_copy_zext(0), rnx_f64::zero(n)); - // should be an uninit read - ASSERT_TRUE(!(v.get_copy_zext(2) == rnx_f64::zero(n))); // should be uninit - // should be an invalid read (inter-slice) - ASSERT_NE(v.data()[4], 0); - ASSERT_EQ(v.data()[2], 0); // should be ok - // should be an uninit read - ASSERT_NE(v.data()[13], 0); // should be uninit -} -#endif - -// test of binary operations - -// test for out of place calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_binop_outplace(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {2, 4, 8, 128}) { - RNX_MODULE_TYPE mtype = FFT64; - MOD_RNX* mod = new_rnx_module_info(n, mtype); - for (uint64_t sa : {7, 13, 15}) { - for (uint64_t sb : {7, 13, 15}) { - for (uint64_t sc : {7, 13, 15}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - uint64_t c_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - rnx_vec_f64_layout lb(n, sb, b_sl); - rnx_vec_f64_layout lc(n, sc, c_sl); - std::vector<rnx_f64> expect(sc); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sb; ++i) { - lb.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sc; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), lb.get_copy_zext(i)); - } - binop(mod, // N - lc.data(), sc, c_sl, // res - la.data(), sa, a_sl, // a - lb.data(), sb, b_sl); - for (uint64_t i = 0; i < sc; ++i) { - ASSERT_EQ(lc.get_copy_zext(i), expect[i]); - } - } - } - } - delete_rnx_module_info(mod); - } -} -// test for inplace1 calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_binop_inplace1(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {2, 4, 64}) { - RNX_MODULE_TYPE mtype = FFT64; - MOD_RNX* mod = new_rnx_module_info(n, mtype); - for (uint64_t sa : {3, 9, 12}) { - for (uint64_t sb : {3, 9, 12}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - rnx_vec_f64_layout lb(n, sb, b_sl); - std::vector<rnx_f64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sb; ++i) { - lb.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), lb.get_copy_zext(i)); - } - binop(mod, // N - la.data(), sa, a_sl, // res - la.data(), sa, a_sl, // a - lb.data(), sb, b_sl); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]); - } - } - } - delete_rnx_module_info(mod); - } -} -// test for inplace2 calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_binop_inplace2(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {4, 32, 64}) { - RNX_MODULE_TYPE mtype = FFT64; - MOD_RNX* mod = new_rnx_module_info(n, mtype); - for (uint64_t sa : {3, 9, 12}) { - for (uint64_t sb : {3, 9, 12}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - rnx_vec_f64_layout lb(n, sb, b_sl); - std::vector<rnx_f64> expect(sb); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sb; ++i) { - lb.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sb; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), lb.get_copy_zext(i)); - } - binop(mod, // N - lb.data(), sb, b_sl, // res - la.data(), sa, a_sl, // a - lb.data(), sb, b_sl); - for (uint64_t i = 0; i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), expect[i]); - } - } - } - delete_rnx_module_info(mod); - } -} -// test for inplace3 calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_binop_inplace3(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {2, 16, 1024}) { - RNX_MODULE_TYPE mtype = FFT64; - MOD_RNX* mod = new_rnx_module_info(n, mtype); - for (uint64_t sa : {2, 6, 11}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - std::vector<rnx_f64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 1.)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), la.get_copy_zext(i)); - } - binop(mod, // N - la.data(), sa, a_sl, // res - la.data(), sa, a_sl, // a - la.data(), sa, a_sl); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]); - } - } - delete_rnx_module_info(mod); - } -} -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_binop(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - test_vec_rnx_elemw_binop_outplace(binop, ref_binop); - test_vec_rnx_elemw_binop_inplace1(binop, ref_binop); - test_vec_rnx_elemw_binop_inplace2(binop, ref_binop); - test_vec_rnx_elemw_binop_inplace3(binop, ref_binop); -} - -static rnx_f64 poly_add(const rnx_f64& a, const rnx_f64& b) { return a + b; } -static rnx_f64 poly_sub(const rnx_f64& a, const rnx_f64& b) { return a - b; } -TEST(vec_rnx, vec_rnx_add) { test_vec_rnx_elemw_binop(vec_rnx_add, poly_add); } -TEST(vec_rnx, vec_rnx_add_ref) { test_vec_rnx_elemw_binop(vec_rnx_add_ref, poly_add); } -#ifdef __x86_64__ -TEST(vec_rnx, vec_rnx_add_avx) { test_vec_rnx_elemw_binop(vec_rnx_add_avx, poly_add); } -#endif -TEST(vec_rnx, vec_rnx_sub) { test_vec_rnx_elemw_binop(vec_rnx_sub, poly_sub); } -TEST(vec_rnx, vec_rnx_sub_ref) { test_vec_rnx_elemw_binop(vec_rnx_sub_ref, poly_sub); } -#ifdef __x86_64__ -TEST(vec_rnx, vec_rnx_sub_avx) { test_vec_rnx_elemw_binop(vec_rnx_sub_avx, poly_sub); } -#endif - -// test for out of place calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_unop_param_outplace(ACTUAL_FCN test_mul_xp_minus_one, EXPECT_FCN ref_mul_xp_minus_one, - int64_t (*param_gen)()) { - for (uint64_t n : {2, 4, 8, 128}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - for (uint64_t sa : {7, 13, 15}) { - for (uint64_t sb : {7, 13, 15}) { - { - int64_t p = param_gen(); - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 4 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - rnx_vec_f64_layout lb(n, sb, b_sl); - std::vector<rnx_f64> expect(sb); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 0)); - } - for (uint64_t i = 0; i < sb; ++i) { - expect[i] = ref_mul_xp_minus_one(p, la.get_copy_zext(i)); - } - test_mul_xp_minus_one(mod, // - p, // - lb.data(), sb, b_sl, // - la.data(), sa, a_sl // - ); - for (uint64_t i = 0; i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), expect[i]) << n << " " << sa << " " << sb << " " << i; - } - } - } - } - delete_rnx_module_info(mod); - } -} - -// test for inplace calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_unop_param_inplace(ACTUAL_FCN actual_function, EXPECT_FCN ref_function, - int64_t (*param_gen)()) { - for (uint64_t n : {2, 16, 1024}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - for (uint64_t sa : {2, 6, 11}) { - { - int64_t p = param_gen(); - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - std::vector<rnx_f64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 0)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_function(p, la.get_copy_zext(i)); - } - actual_function(mod, // N - p, //; - la.data(), sa, a_sl, // res - la.data(), sa, a_sl // a - ); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]) << n << " " << sa << " " << i; - } - } - } - delete_rnx_module_info(mod); - } -} - -static int64_t random_mul_xp_minus_one_param() { return uniform_i64(); } -static int64_t random_automorphism_param() { return 2 * uniform_i64() + 1; } -static int64_t random_rotation_param() { return uniform_i64(); } - -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_mul_xp_minus_one(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - test_vec_rnx_elemw_unop_param_outplace(binop, ref_binop, random_mul_xp_minus_one_param); - test_vec_rnx_elemw_unop_param_inplace(binop, ref_binop, random_mul_xp_minus_one_param); -} -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_rotate(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - test_vec_rnx_elemw_unop_param_outplace(binop, ref_binop, random_rotation_param); - test_vec_rnx_elemw_unop_param_inplace(binop, ref_binop, random_rotation_param); -} -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_automorphism(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - test_vec_rnx_elemw_unop_param_outplace(binop, ref_binop, random_automorphism_param); - test_vec_rnx_elemw_unop_param_inplace(binop, ref_binop, random_automorphism_param); -} - -static rnx_f64 poly_mul_xp_minus_one(const int64_t p, const rnx_f64& a) { - uint64_t n = a.nn(); - rnx_f64 res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, a.get_coeff(i - p) - a.get_coeff(i)); - } - return res; -} -static rnx_f64 poly_rotate(const int64_t p, const rnx_f64& a) { - uint64_t n = a.nn(); - rnx_f64 res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, a.get_coeff(i - p)); - } - return res; -} -static rnx_f64 poly_automorphism(const int64_t p, const rnx_f64& a) { - uint64_t n = a.nn(); - rnx_f64 res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i * p, a.get_coeff(i)); - } - return res; -} - -TEST(vec_rnx, vec_rnx_mul_xp_minus_one) { - test_vec_rnx_elemw_mul_xp_minus_one(vec_rnx_mul_xp_minus_one, poly_mul_xp_minus_one); -} -TEST(vec_rnx, vec_rnx_mul_xp_minus_one_ref) { - test_vec_rnx_elemw_mul_xp_minus_one(vec_rnx_mul_xp_minus_one_ref, poly_mul_xp_minus_one); -} - -TEST(vec_rnx, vec_rnx_rotate) { test_vec_rnx_elemw_rotate(vec_rnx_rotate, poly_rotate); } -TEST(vec_rnx, vec_rnx_rotate_ref) { test_vec_rnx_elemw_rotate(vec_rnx_rotate_ref, poly_rotate); } -TEST(vec_rnx, vec_rnx_automorphism) { test_vec_rnx_elemw_automorphism(vec_rnx_automorphism, poly_automorphism); } -TEST(vec_rnx, vec_rnx_automorphism_ref) { - test_vec_rnx_elemw_automorphism(vec_rnx_automorphism_ref, poly_automorphism); -} - -// test for out of place calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_unop_outplace(ACTUAL_FCN actual_function, EXPECT_FCN ref_function) { - for (uint64_t n : {2, 4, 8, 128}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - for (uint64_t sa : {7, 13, 15}) { - for (uint64_t sb : {7, 13, 15}) { - { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 4 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - rnx_vec_f64_layout lb(n, sb, b_sl); - std::vector<rnx_f64> expect(sb); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 0)); - } - for (uint64_t i = 0; i < sb; ++i) { - expect[i] = ref_function(la.get_copy_zext(i)); - } - actual_function(mod, // - lb.data(), sb, b_sl, // - la.data(), sa, a_sl // - ); - for (uint64_t i = 0; i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), expect[i]) << n << " " << sa << " " << sb << " " << i; - } - } - } - } - delete_rnx_module_info(mod); - } -} - -// test for inplace calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_unop_inplace(ACTUAL_FCN actual_function, EXPECT_FCN ref_function) { - for (uint64_t n : {2, 16, 1024}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - for (uint64_t sa : {2, 6, 11}) { - { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - std::vector<rnx_f64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 0)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_function(la.get_copy_zext(i)); - } - actual_function(mod, // N - la.data(), sa, a_sl, // res - la.data(), sa, a_sl // a - ); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]) << n << " " << sa << " " << i; - } - } - } - delete_rnx_module_info(mod); - } -} -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_rnx_elemw_unop(ACTUAL_FCN unnop, EXPECT_FCN ref_unnop) { - test_vec_rnx_elemw_unop_outplace(unnop, ref_unnop); - test_vec_rnx_elemw_unop_inplace(unnop, ref_unnop); -} - -static rnx_f64 poly_copy(const rnx_f64& a) { return a; } -static rnx_f64 poly_negate(const rnx_f64& a) { return -a; } - -TEST(vec_rnx, vec_rnx_copy) { test_vec_rnx_elemw_unop(vec_rnx_copy, poly_copy); } -TEST(vec_rnx, vec_rnx_copy_ref) { test_vec_rnx_elemw_unop(vec_rnx_copy_ref, poly_copy); } -TEST(vec_rnx, vec_rnx_negate) { test_vec_rnx_elemw_unop(vec_rnx_negate, poly_negate); } -TEST(vec_rnx, vec_rnx_negate_ref) { test_vec_rnx_elemw_unop(vec_rnx_negate_ref, poly_negate); } -#ifdef __x86_64__ -TEST(vec_rnx, vec_rnx_negate_avx) { test_vec_rnx_elemw_unop(vec_rnx_negate_avx, poly_negate); } -#endif - -// test for inplace calls -void test_vec_rnx_zero(VEC_RNX_ZERO_F actual_function) { - for (uint64_t n : {2, 16, 1024}) { - MOD_RNX* mod = new_rnx_module_info(n, FFT64); - for (uint64_t sa : {2, 6, 11}) { - { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - rnx_vec_f64_layout la(n, sa, a_sl); - const rnx_f64 ZERO = rnx_f64::zero(n); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, rnx_f64::random_log2bound(n, 0)); - } - actual_function(mod, // N - la.data(), sa, a_sl // res - ); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), ZERO) << n << " " << sa << " " << i; - } - } - } - delete_rnx_module_info(mod); - } -} - -TEST(vec_rnx, vec_rnx_zero) { test_vec_rnx_zero(vec_rnx_zero); } - -TEST(vec_rnx, vec_rnx_zero_ref) { test_vec_rnx_zero(vec_rnx_zero_ref); }
@@ -1,369 +0,0 @@
-#include "../spqlios/arithmetic/vec_rnx_arithmetic_private.h" -#include "../spqlios/reim/reim_fft.h" -#include "gtest/gtest.h" -#include "testlib/vec_rnx_layout.h" - -static void test_vmp_apply_dft_to_dft_outplace( // - RNX_VMP_APPLY_DFT_TO_DFT_F* apply, // - RNX_VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* tmp_bytes) { - for (uint64_t nn : {2, 4, 8, 64}) { - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t mat_nrows : {1, 4, 7}) { - for (uint64_t mat_ncols : {1, 2, 5}) { - for (uint64_t in_size : {1, 4, 7}) { - for (uint64_t out_size : {1, 2, 5}) { - const uint64_t in_sl = nn + uniform_u64_bits(2); - const uint64_t out_sl = nn + uniform_u64_bits(2); - rnx_vec_f64_layout in(nn, in_size, in_sl); - fft64_rnx_vmp_pmat_layout pmat(nn, mat_nrows, mat_ncols); - rnx_vec_f64_layout out(nn, out_size, out_sl); - in.fill_random(0); - pmat.fill_random(0); - // naive computation of the product - std::vector<reim_fft64vec> expect(out_size, reim_fft64vec(nn)); - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec ex = reim_fft64vec::zero(nn); - for (uint64_t row = 0; row < std::min(mat_nrows, in_size); ++row) { - ex += pmat.get_zext(row, col) * in.get_dft_copy(row); - } - expect[col] = ex; - } - // apply the product - std::vector<uint8_t> tmp(tmp_bytes(module, out_size, in_size, mat_nrows, mat_ncols)); - apply(module, // - out.data(), out_size, out_sl, // - in.data(), in_size, in_sl, // - pmat.data, mat_nrows, mat_ncols, // - tmp.data()); - // check that the output is close from the expectation - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec actual = out.get_dft_copy_zext(col); - ASSERT_LE(infty_dist(actual, expect[col]), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -static void test_vmp_apply_dft_to_dft_inplace( // - RNX_VMP_APPLY_DFT_TO_DFT_F* apply, // - RNX_VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* tmp_bytes) { - for (uint64_t nn : {2, 4, 8, 64}) { - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t mat_nrows : {1, 2, 6}) { - for (uint64_t mat_ncols : {1, 2, 7, 8}) { - for (uint64_t in_size : {1, 3, 6}) { - for (uint64_t out_size : {1, 3, 6}) { - const uint64_t in_out_sl = nn + uniform_u64_bits(2); - rnx_vec_f64_layout in_out(nn, std::max(in_size, out_size), in_out_sl); - fft64_rnx_vmp_pmat_layout pmat(nn, mat_nrows, mat_ncols); - in_out.fill_random(0); - pmat.fill_random(0); - // naive computation of the product - std::vector<reim_fft64vec> expect(out_size, reim_fft64vec(nn)); - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec ex = reim_fft64vec::zero(nn); - for (uint64_t row = 0; row < std::min(mat_nrows, in_size); ++row) { - ex += pmat.get_zext(row, col) * in_out.get_dft_copy(row); - } - expect[col] = ex; - } - // apply the product - std::vector<uint8_t> tmp(tmp_bytes(module, out_size, in_size, mat_nrows, mat_ncols)); - apply(module, // - in_out.data(), out_size, in_out_sl, // - in_out.data(), in_size, in_out_sl, // - pmat.data, mat_nrows, mat_ncols, // - tmp.data()); - // check that the output is close from the expectation - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec actual = in_out.get_dft_copy_zext(col); - ASSERT_LE(infty_dist(actual, expect[col]), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -static void test_vmp_apply_dft_to_dft( // - RNX_VMP_APPLY_DFT_TO_DFT_F* apply, // - RNX_VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* tmp_bytes) { - test_vmp_apply_dft_to_dft_outplace(apply, tmp_bytes); - test_vmp_apply_dft_to_dft_inplace(apply, tmp_bytes); -} - -TEST(vec_rnx, vmp_apply_to_dft) { - test_vmp_apply_dft_to_dft(rnx_vmp_apply_dft_to_dft, rnx_vmp_apply_dft_to_dft_tmp_bytes); -} -TEST(vec_rnx, fft64_vmp_apply_dft_to_dft_ref) { - test_vmp_apply_dft_to_dft(fft64_rnx_vmp_apply_dft_to_dft_ref, fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_ref); -} -#ifdef __x86_64__ -TEST(vec_rnx, fft64_vmp_apply_dft_to_dft_avx) { - test_vmp_apply_dft_to_dft(fft64_rnx_vmp_apply_dft_to_dft_avx, fft64_rnx_vmp_apply_dft_to_dft_tmp_bytes_avx); -} -#endif - -/// rnx_vmp_prepare - -static void test_vmp_prepare_contiguous(RNX_VMP_PREPARE_CONTIGUOUS_F* prepare_contiguous, - RNX_VMP_PREPARE_TMP_BYTES_F* tmp_bytes) { - // tests when n < 8 - for (uint64_t nn : {2, 4}) { - const double one_over_m = 2. / nn; - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t nrows : {1, 2, 5}) { - for (uint64_t ncols : {2, 6, 7}) { - rnx_vec_f64_layout mat(nn, nrows * ncols, nn); - fft64_rnx_vmp_pmat_layout pmat(nn, nrows, ncols); - mat.fill_random(0); - std::vector<uint8_t> tmp_space(tmp_bytes(module)); - thash hash_before = mat.content_hash(); - prepare_contiguous(module, pmat.data, mat.data(), nrows, ncols, tmp_space.data()); - ASSERT_EQ(mat.content_hash(), hash_before); - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - const double* pmatv = (double*)pmat.data + (col * nrows + row) * nn; - reim_fft64vec tmp = one_over_m * simple_fft64(mat.get_copy(row * ncols + col)); - const double* tmpv = tmp.data(); - for (uint64_t i = 0; i < nn; ++i) { - ASSERT_LE(abs(pmatv[i] - tmpv[i]), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } - // tests when n >= 8 - for (uint64_t nn : {8, 32}) { - const double one_over_m = 2. / nn; - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - uint64_t nblk = nn / 8; - for (uint64_t nrows : {1, 2, 5}) { - for (uint64_t ncols : {2, 6, 7}) { - rnx_vec_f64_layout mat(nn, nrows * ncols, nn); - fft64_rnx_vmp_pmat_layout pmat(nn, nrows, ncols); - mat.fill_random(0); - std::vector<uint8_t> tmp_space(tmp_bytes(module)); - thash hash_before = mat.content_hash(); - prepare_contiguous(module, pmat.data, mat.data(), nrows, ncols, tmp_space.data()); - ASSERT_EQ(mat.content_hash(), hash_before); - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - reim_fft64vec tmp = one_over_m * simple_fft64(mat.get_copy(row * ncols + col)); - for (uint64_t blk = 0; blk < nblk; ++blk) { - reim4_elem expect = tmp.get_blk(blk); - reim4_elem actual = pmat.get(row, col, blk); - ASSERT_LE(infty_dist(actual, expect), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -TEST(vec_rnx, vmp_prepare_contiguous) { - test_vmp_prepare_contiguous(rnx_vmp_prepare_contiguous, rnx_vmp_prepare_tmp_bytes); -} -TEST(vec_rnx, fft64_vmp_prepare_contiguous_ref) { - test_vmp_prepare_contiguous(fft64_rnx_vmp_prepare_contiguous_ref, fft64_rnx_vmp_prepare_tmp_bytes_ref); -} -#ifdef __x86_64__ -TEST(vec_rnx, fft64_vmp_prepare_contiguous_avx) { - test_vmp_prepare_contiguous(fft64_rnx_vmp_prepare_contiguous_avx, fft64_rnx_vmp_prepare_tmp_bytes_avx); -} -#endif - -/// rnx_vmp_prepare_dblptr - -static void test_vmp_prepare_dblptr(RNX_VMP_PREPARE_DBLPTR_F* prepare_dblptr, RNX_VMP_PREPARE_TMP_BYTES_F* tmp_bytes) { - // tests when n < 8 - for (uint64_t nn : {2, 4}) { - const double one_over_m = 2. / nn; - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t nrows : {1, 2, 5}) { - for (uint64_t ncols : {2, 6, 7}) { - rnx_vec_f64_layout mat(nn, nrows * ncols, nn); - fft64_rnx_vmp_pmat_layout pmat(nn, nrows, ncols); - mat.fill_random(0); - std::vector<uint8_t> tmp_space(tmp_bytes(module)); - thash hash_before = mat.content_hash(); - const double** mat_dblptr = (const double**)malloc(nrows * sizeof(double*)); - for (size_t row_i = 0; row_i < nrows; row_i++) { - mat_dblptr[row_i] = &mat.data()[row_i * ncols * nn]; - }; - prepare_dblptr(module, pmat.data, mat_dblptr, nrows, ncols, tmp_space.data()); - ASSERT_EQ(mat.content_hash(), hash_before); - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - const double* pmatv = (double*)pmat.data + (col * nrows + row) * nn; - reim_fft64vec tmp = one_over_m * simple_fft64(mat.get_copy(row * ncols + col)); - const double* tmpv = tmp.data(); - for (uint64_t i = 0; i < nn; ++i) { - ASSERT_LE(abs(pmatv[i] - tmpv[i]), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } - // tests when n >= 8 - for (uint64_t nn : {8, 32}) { - const double one_over_m = 2. / nn; - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - uint64_t nblk = nn / 8; - for (uint64_t nrows : {1, 2, 5}) { - for (uint64_t ncols : {2, 6, 7}) { - rnx_vec_f64_layout mat(nn, nrows * ncols, nn); - fft64_rnx_vmp_pmat_layout pmat(nn, nrows, ncols); - mat.fill_random(0); - std::vector<uint8_t> tmp_space(tmp_bytes(module)); - thash hash_before = mat.content_hash(); - const double** mat_dblptr = (const double**)malloc(nrows * sizeof(double*)); - for (size_t row_i = 0; row_i < nrows; row_i++) { - mat_dblptr[row_i] = &mat.data()[row_i * ncols * nn]; - }; - prepare_dblptr(module, pmat.data, mat_dblptr, nrows, ncols, tmp_space.data()); - ASSERT_EQ(mat.content_hash(), hash_before); - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - reim_fft64vec tmp = one_over_m * simple_fft64(mat.get_copy(row * ncols + col)); - for (uint64_t blk = 0; blk < nblk; ++blk) { - reim4_elem expect = tmp.get_blk(blk); - reim4_elem actual = pmat.get(row, col, blk); - ASSERT_LE(infty_dist(actual, expect), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -TEST(vec_rnx, vmp_prepare_dblptr) { test_vmp_prepare_dblptr(rnx_vmp_prepare_dblptr, rnx_vmp_prepare_tmp_bytes); } -TEST(vec_rnx, fft64_vmp_prepare_dblptr_ref) { - test_vmp_prepare_dblptr(fft64_rnx_vmp_prepare_dblptr_ref, fft64_rnx_vmp_prepare_tmp_bytes_ref); -} -#ifdef __x86_64__ -TEST(vec_rnx, fft64_vmp_prepare_dblptr_avx) { - test_vmp_prepare_dblptr(fft64_rnx_vmp_prepare_dblptr_avx, fft64_rnx_vmp_prepare_tmp_bytes_avx); -} -#endif - -/// rnx_vmp_apply_dft_to_dft - -static void test_vmp_apply_tmp_a_outplace( // - RNX_VMP_APPLY_TMP_A_F* apply, // - RNX_VMP_APPLY_TMP_A_TMP_BYTES_F* tmp_bytes) { - for (uint64_t nn : {2, 4, 8, 64}) { - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t mat_nrows : {1, 4, 7}) { - for (uint64_t mat_ncols : {1, 2, 5}) { - for (uint64_t in_size : {1, 4, 7}) { - for (uint64_t out_size : {1, 2, 5}) { - const uint64_t in_sl = nn + uniform_u64_bits(2); - const uint64_t out_sl = nn + uniform_u64_bits(2); - rnx_vec_f64_layout in(nn, in_size, in_sl); - fft64_rnx_vmp_pmat_layout pmat(nn, mat_nrows, mat_ncols); - rnx_vec_f64_layout out(nn, out_size, out_sl); - in.fill_random(0); - pmat.fill_random(0); - // naive computation of the product - std::vector<rnx_f64> expect(out_size); - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec ex = reim_fft64vec::zero(nn); - for (uint64_t row = 0; row < std::min(mat_nrows, in_size); ++row) { - ex += pmat.get_zext(row, col) * simple_fft64(in.get_copy(row)); - } - expect[col] = simple_ifft64(ex); - } - // apply the product - std::vector<uint8_t> tmp(tmp_bytes(module, out_size, in_size, mat_nrows, mat_ncols)); - apply(module, // - out.data(), out_size, out_sl, // - in.data(), in_size, in_sl, // - pmat.data, mat_nrows, mat_ncols, // - tmp.data()); - // check that the output is close from the expectation - for (uint64_t col = 0; col < out_size; ++col) { - rnx_f64 actual = out.get_copy_zext(col); - ASSERT_LE(infty_dist(actual, expect[col]), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -static void test_vmp_apply_tmp_a_inplace( // - RNX_VMP_APPLY_TMP_A_F* apply, // - RNX_VMP_APPLY_TMP_A_TMP_BYTES_F* tmp_bytes) { - for (uint64_t nn : {2, 4, 8, 64}) { - MOD_RNX* module = new_rnx_module_info(nn, FFT64); - for (uint64_t mat_nrows : {1, 4, 7}) { - for (uint64_t mat_ncols : {1, 2, 5}) { - for (uint64_t in_size : {1, 4, 7}) { - for (uint64_t out_size : {1, 2, 5}) { - const uint64_t in_out_sl = nn + uniform_u64_bits(2); - rnx_vec_f64_layout in_out(nn, std::max(in_size, out_size), in_out_sl); - fft64_rnx_vmp_pmat_layout pmat(nn, mat_nrows, mat_ncols); - in_out.fill_random(0); - pmat.fill_random(0); - // naive computation of the product - std::vector<rnx_f64> expect(out_size); - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec ex = reim_fft64vec::zero(nn); - for (uint64_t row = 0; row < std::min(mat_nrows, in_size); ++row) { - ex += pmat.get_zext(row, col) * simple_fft64(in_out.get_copy(row)); - } - expect[col] = simple_ifft64(ex); - } - // apply the product - std::vector<uint8_t> tmp(tmp_bytes(module, out_size, in_size, mat_nrows, mat_ncols)); - apply(module, // - in_out.data(), out_size, in_out_sl, // - in_out.data(), in_size, in_out_sl, // - pmat.data, mat_nrows, mat_ncols, // - tmp.data()); - // check that the output is close from the expectation - for (uint64_t col = 0; col < out_size; ++col) { - rnx_f64 actual = in_out.get_copy_zext(col); - ASSERT_LE(infty_dist(actual, expect[col]), 1e-10); - } - } - } - } - } - delete_rnx_module_info(module); - } -} - -static void test_vmp_apply_tmp_a( // - RNX_VMP_APPLY_TMP_A_F* apply, // - RNX_VMP_APPLY_TMP_A_TMP_BYTES_F* tmp_bytes) { - test_vmp_apply_tmp_a_outplace(apply, tmp_bytes); - test_vmp_apply_tmp_a_inplace(apply, tmp_bytes); -} - -TEST(vec_znx, fft64_vmp_apply_tmp_a) { test_vmp_apply_tmp_a(rnx_vmp_apply_tmp_a, rnx_vmp_apply_tmp_a_tmp_bytes); } -TEST(vec_znx, fft64_vmp_apply_tmp_a_ref) { - test_vmp_apply_tmp_a(fft64_rnx_vmp_apply_tmp_a_ref, fft64_rnx_vmp_apply_tmp_a_tmp_bytes_ref); -} -#ifdef __x86_64__ -TEST(vec_znx, fft64_vmp_apply_tmp_a_avx) { - test_vmp_apply_tmp_a(fft64_rnx_vmp_apply_tmp_a_avx, fft64_rnx_vmp_apply_tmp_a_tmp_bytes_avx); -} -#endif
@@ -1,265 +0,0 @@
-#include <gtest/gtest.h> - -#include "spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "test/testlib/polynomial_vector.h" -#include "testlib/fft64_layouts.h" -#include "testlib/test_commons.h" - -#define def_rand_big(varname, ringdim, varsize) \ - fft64_vec_znx_big_layout varname(ringdim, varsize); \ - varname.fill_random() - -#define def_rand_small(varname, ringdim, varsize) \ - znx_vec_i64_layout varname(ringdim, varsize, 2 * ringdim); \ - varname.fill_random() - -#define test_prelude(ringdim, moduletype, dim1, dim2, dim3) \ - uint64_t n = ringdim; \ - MODULE* module = new_module_info(ringdim, moduletype); \ - for (uint64_t sa : {dim1, dim2, dim3}) { \ - for (uint64_t sb : {dim1, dim2, dim3}) { \ - for (uint64_t sr : {dim1, dim2, dim3}) - -#define test_end() \ - } \ - } \ - free(module) - -void test_fft64_vec_znx_big_add(VEC_ZNX_BIG_ADD_F vec_znx_big_add_fcn) { - test_prelude(8, FFT64, 3, 5, 7) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_big(a, n, sa); - def_rand_big(b, n, sb); - vec_znx_big_add_fcn(module, r.data, sr, a.data, sa, b.data, sb); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) + b.get_copy_zext(i)); - } - } - test_end(); -} -void test_fft64_vec_znx_big_add_small(VEC_ZNX_BIG_ADD_SMALL_F vec_znx_big_add_fcn) { - test_prelude(16, FFT64, 2, 4, 5) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_big(a, n, sa); - def_rand_small(b, n, sb); - vec_znx_big_add_fcn(module, r.data, sr, a.data, sa, b.data(), sb, 2 * n); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) + b.get_copy_zext(i)); - } - } - test_end(); -} -void test_fft64_vec_znx_big_add_small2(VEC_ZNX_BIG_ADD_SMALL2_F vec_znx_big_add_fcn) { - test_prelude(64, FFT64, 3, 6, 7) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_small(a, n, sa); - def_rand_small(b, n, sb); - vec_znx_big_add_fcn(module, r.data, sr, a.data(), sa, 2 * n, b.data(), sb, 2 * n); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) + b.get_copy_zext(i)); - } - } - test_end(); -} - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_add) { test_fft64_vec_znx_big_add(fft64_vec_znx_big_add); } -TEST(vec_znx_big, vec_znx_big_add) { test_fft64_vec_znx_big_add(vec_znx_big_add); } - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_add_small) { test_fft64_vec_znx_big_add_small(fft64_vec_znx_big_add_small); } -TEST(vec_znx_big, vec_znx_big_add_small) { test_fft64_vec_znx_big_add_small(vec_znx_big_add_small); } - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_add_small2) { - test_fft64_vec_znx_big_add_small2(fft64_vec_znx_big_add_small2); -} -TEST(vec_znx_big, vec_znx_big_add_small2) { test_fft64_vec_znx_big_add_small2(vec_znx_big_add_small2); } - -void test_fft64_vec_znx_big_sub(VEC_ZNX_BIG_SUB_F vec_znx_big_sub_fcn) { - test_prelude(16, FFT64, 3, 5, 7) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_big(a, n, sa); - def_rand_big(b, n, sb); - vec_znx_big_sub_fcn(module, r.data, sr, a.data, sa, b.data, sb); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) - b.get_copy_zext(i)); - } - } - test_end(); -} -void test_fft64_vec_znx_big_sub_small_a(VEC_ZNX_BIG_SUB_SMALL_A_F vec_znx_big_sub_fcn) { - test_prelude(32, FFT64, 2, 4, 5) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_small(a, n, sa); - def_rand_big(b, n, sb); - vec_znx_big_sub_fcn(module, r.data, sr, a.data(), sa, 2 * n, b.data, sb); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) - b.get_copy_zext(i)); - } - } - test_end(); -} -void test_fft64_vec_znx_big_sub_small_b(VEC_ZNX_BIG_SUB_SMALL_B_F vec_znx_big_sub_fcn) { - test_prelude(16, FFT64, 2, 4, 5) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_big(a, n, sa); - def_rand_small(b, n, sb); - vec_znx_big_sub_fcn(module, r.data, sr, a.data, sa, b.data(), sb, 2 * n); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) - b.get_copy_zext(i)); - } - } - test_end(); -} -void test_fft64_vec_znx_big_sub_small2(VEC_ZNX_BIG_SUB_SMALL2_F vec_znx_big_sub_fcn) { - test_prelude(8, FFT64, 3, 6, 7) { - fft64_vec_znx_big_layout r(n, sr); - def_rand_small(a, n, sa); - def_rand_small(b, n, sb); - vec_znx_big_sub_fcn(module, r.data, sr, a.data(), sa, 2 * n, b.data(), sb, 2 * n); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), a.get_copy_zext(i) - b.get_copy_zext(i)); - } - } - test_end(); -} - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_sub) { test_fft64_vec_znx_big_sub(fft64_vec_znx_big_sub); } -TEST(vec_znx_big, vec_znx_big_sub) { test_fft64_vec_znx_big_sub(vec_znx_big_sub); } - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_sub_small_a) { - test_fft64_vec_znx_big_sub_small_a(fft64_vec_znx_big_sub_small_a); -} -TEST(vec_znx_big, vec_znx_big_sub_small_a) { test_fft64_vec_znx_big_sub_small_a(vec_znx_big_sub_small_a); } - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_sub_small_b) { - test_fft64_vec_znx_big_sub_small_b(fft64_vec_znx_big_sub_small_b); -} -TEST(vec_znx_big, vec_znx_big_sub_small_b) { test_fft64_vec_znx_big_sub_small_b(vec_znx_big_sub_small_b); } - -TEST(fft64_vec_znx_big, fft64_vec_znx_big_sub_small2) { - test_fft64_vec_znx_big_sub_small2(fft64_vec_znx_big_sub_small2); -} -TEST(vec_znx_big, vec_znx_big_sub_small2) { test_fft64_vec_znx_big_sub_small2(vec_znx_big_sub_small2); } - -static void test_vec_znx_big_normalize(VEC_ZNX_BIG_NORMALIZE_BASE2K_F normalize, - VEC_ZNX_BIG_NORMALIZE_BASE2K_TMP_BYTES_F normalize_tmp_bytes) { - // in the FFT64 case, big_normalize is just a forward. - // we will just test that the functions are callable - uint64_t n = 16; - uint64_t k = 12; - MODULE* module = new_module_info(n, FFT64); - for (uint64_t sa : {3, 5, 7}) { - for (uint64_t sr : {3, 5, 7}) { - uint64_t r_sl = n + 3; - def_rand_big(a, n, sa); - znx_vec_i64_layout r(n, sr, r_sl); - std::vector<uint8_t> tmp_space(normalize_tmp_bytes(module, n)); - normalize(module, n, k, r.data(), sr, r_sl, a.data, sa, tmp_space.data()); - } - } - delete_module_info(module); -} - -TEST(vec_znx_big, fft64_vec_znx_big_normalize_base2k) { - test_vec_znx_big_normalize(fft64_vec_znx_big_normalize_base2k, fft64_vec_znx_big_normalize_base2k_tmp_bytes); -} -TEST(vec_znx_big, vec_znx_big_normalize_base2k) { - test_vec_znx_big_normalize(vec_znx_big_normalize_base2k, vec_znx_big_normalize_base2k_tmp_bytes); -} - -static void test_vec_znx_big_range_normalize( // - VEC_ZNX_BIG_RANGE_NORMALIZE_BASE2K_F normalize, - VEC_ZNX_BIG_RANGE_NORMALIZE_BASE2K_TMP_BYTES_F normalize_tmp_bytes) { - // in the FFT64 case, big_normalize is just a forward. - // we will just test that the functions are callable - uint64_t n = 16; - uint64_t k = 11; - MODULE* module = new_module_info(n, FFT64); - for (uint64_t sa : {6, 15, 21}) { - for (uint64_t sr : {3, 5, 7}) { - uint64_t r_sl = n + 3; - def_rand_big(a, n, sa); - uint64_t a_start = uniform_u64_bits(30) % (sa / 2); - uint64_t a_end = sa - (uniform_u64_bits(30) % (sa / 2)); - uint64_t a_step = (uniform_u64_bits(30) % 3) + 1; - uint64_t range_size = (a_end + a_step - 1 - a_start) / a_step; - fft64_vec_znx_big_layout aextr(n, range_size); - for (uint64_t i = 0, idx = a_start; idx < a_end; ++i, idx += a_step) { - aextr.set(i, a.get_copy(idx)); - } - znx_vec_i64_layout r(n, sr, r_sl); - znx_vec_i64_layout r2(n, sr, r_sl); - // tmp_space is large-enough for both - std::vector<uint8_t> tmp_space(normalize_tmp_bytes(module, n)); - normalize(module, n, k, r.data(), sr, r_sl, a.data, a_start, a_end, a_step, tmp_space.data()); - fft64_vec_znx_big_normalize_base2k(module, n, k, r2.data(), sr, r_sl, aextr.data, range_size, tmp_space.data()); - for (uint64_t i = 0; i < sr; ++i) { - ASSERT_EQ(r.get_copy(i), r2.get_copy(i)); - } - } - } - delete_module_info(module); -} - -TEST(vec_znx_big, fft64_vec_znx_big_range_normalize_base2k) { - test_vec_znx_big_range_normalize(fft64_vec_znx_big_range_normalize_base2k, - fft64_vec_znx_big_range_normalize_base2k_tmp_bytes); -} -TEST(vec_znx_big, vec_znx_big_range_normalize_base2k) { - test_vec_znx_big_range_normalize(vec_znx_big_range_normalize_base2k, vec_znx_big_range_normalize_base2k_tmp_bytes); -} - -static void test_vec_znx_big_rotate(VEC_ZNX_BIG_ROTATE_F rotate) { - // in the FFT64 case, big_normalize is just a forward. - // we will just test that the functions are callable - uint64_t n = 16; - int64_t p = 12; - MODULE* module = new_module_info(n, FFT64); - for (uint64_t sa : {3, 5, 7}) { - for (uint64_t sr : {3, 5, 7}) { - def_rand_big(a, n, sa); - fft64_vec_znx_big_layout r(n, sr); - rotate(module, p, r.data, sr, a.data, sa); - for (uint64_t i = 0; i < sr; ++i) { - znx_i64 aa = a.get_copy_zext(i); - znx_i64 expect(n); - for (uint64_t j = 0; j < n; ++j) { - expect.set_coeff(j, aa.get_coeff(int64_t(j) - p)); - } - znx_i64 actual = r.get_copy(i); - ASSERT_EQ(expect, actual); - } - } - } - delete_module_info(module); -} - -TEST(vec_znx_big, fft64_vec_znx_big_rotate) { test_vec_znx_big_rotate(fft64_vec_znx_big_rotate); } -TEST(vec_znx_big, vec_znx_big_rotate) { test_vec_znx_big_rotate(vec_znx_big_rotate); } - -static void test_vec_znx_big_automorphism(VEC_ZNX_BIG_AUTOMORPHISM_F automorphism) { - // in the FFT64 case, big_normalize is just a forward. - // we will just test that the functions are callable - uint64_t n = 16; - int64_t p = 11; - MODULE* module = new_module_info(n, FFT64); - for (uint64_t sa : {3, 5, 7}) { - for (uint64_t sr : {3, 5, 7}) { - def_rand_big(a, n, sa); - fft64_vec_znx_big_layout r(n, sr); - automorphism(module, p, r.data, sr, a.data, sa); - for (uint64_t i = 0; i < sr; ++i) { - znx_i64 aa = a.get_copy_zext(i); - znx_i64 expect(n); - for (uint64_t j = 0; j < n; ++j) { - expect.set_coeff(p * j, aa.get_coeff(j)); - } - znx_i64 actual = r.get_copy(i); - ASSERT_EQ(expect, actual); - } - } - } - delete_module_info(module); -} - -TEST(vec_znx_big, fft64_vec_znx_big_automorphism) { test_vec_znx_big_automorphism(fft64_vec_znx_big_automorphism); } -TEST(vec_znx_big, vec_znx_big_automorphism) { test_vec_znx_big_automorphism(vec_znx_big_automorphism); }
@@ -1,231 +0,0 @@
-#include <gtest/gtest.h> - -#include <cstdint> - -#include "../spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "spqlios/arithmetic/vec_znx_arithmetic.h" -#include "test/testlib/ntt120_dft.h" -#include "test/testlib/ntt120_layouts.h" -#include "testlib/fft64_dft.h" -#include "testlib/fft64_layouts.h" -#include "testlib/polynomial_vector.h" - -static void test_fft64_vec_znx_dft(VEC_ZNX_DFT_F dft) { - for (uint64_t n : {2, 4, 128}) { - MODULE* module = new_module_info(n, FFT64); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - uint64_t a_sl = n + uniform_u64_bits(2); - znx_vec_i64_layout a(n, sa, a_sl); - fft64_vec_znx_dft_layout res(n, sr); - a.fill_random(42); - std::vector<reim_fft64vec> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - expect[i] = simple_fft64(a.get_copy_zext(i)); - } - // test the function - thash hash_before = a.content_hash(); - dft(module, res.data, sr, a.data(), sa, a_sl); - ASSERT_EQ(a.content_hash(), hash_before); - for (uint64_t i = 0; i < sr; ++i) { - reim_fft64vec actual = res.get_copy_zext(i); - ASSERT_LE(infty_dist(actual, expect[i]), 1e-10); - } - } - } - delete_module_info(module); - } -} - -static void test_fft64_vec_dft_add(VEC_DFT_ADD_F dft_add) { - for (uint64_t n : {2, 4, 128}) { - MODULE* module = new_module_info(n, FFT64); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - fft64_vec_znx_dft_layout a(n, sa); - fft64_vec_znx_dft_layout b(n, sa); - fft64_vec_znx_dft_layout res(n, sr); - a.fill_dft_random_log2bound(42); - b.fill_dft_random_log2bound(42); - std::vector<reim_fft64vec> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - std::vector<double> v(n); - if (i < sa) { - reim_fftvec_add(module->mod.fft64.add_fft, v.data(), ((double*)a.data) + i * n, ((double*)b.data) + i * n); - } else { - std::fill(v.begin(), v.end(), 0.0); - } - expect[i] = reim_fft64vec(n, v.data()); - } - // test the function - thash a_hash_before = a.content_hash(); - thash b_hash_before = b.content_hash(); - dft_add(module, res.data, sr, a.data, sa, b.data, sa); - ASSERT_EQ(a.content_hash(), a_hash_before); - ASSERT_EQ(b.content_hash(), b_hash_before); - for (uint64_t i = 0; i < sr; ++i) { - reim_fft64vec actual = res.get_copy_zext(i); - ASSERT_LE(infty_dist(actual, expect[i]), 1e-10); - } - } - } - delete_module_info(module); - } -} - -#ifdef __x86_64__ -// FIXME: currently, it only works on avx -static void test_ntt120_vec_znx_dft(VEC_ZNX_DFT_F dft) { - for (uint64_t n : {2, 4, 128}) { - MODULE* module = new_module_info(n, NTT120); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - uint64_t a_sl = n + uniform_u64_bits(2); - znx_vec_i64_layout a(n, sa, a_sl); - ntt120_vec_znx_dft_layout res(n, sr); - a.fill_random(42); - std::vector<q120_nttvec> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - expect[i] = simple_ntt120(a.get_copy_zext(i)); - } - // test the function - thash hash_before = a.content_hash(); - dft(module, res.data, sr, a.data(), sa, a_sl); - ASSERT_EQ(a.content_hash(), hash_before); - for (uint64_t i = 0; i < sr; ++i) { - q120_nttvec actual = res.get_copy_zext(i); - if (!(actual == expect[i])) { - for (uint64_t j = 0; j < n; ++j) { - std::cerr << actual.v[j] << " vs " << expect[i].v[j] << std::endl; - } - } - ASSERT_EQ(actual, expect[i]); - } - } - } - delete_module_info(module); - } -} -#endif - -TEST(vec_znx_dft, fft64_vec_znx_dft) { test_fft64_vec_znx_dft(fft64_vec_znx_dft); } -#ifdef __x86_64__ -// FIXME: currently, it only works on avx -TEST(vec_znx_dft, ntt120_vec_znx_dft) { test_ntt120_vec_znx_dft(ntt120_vec_znx_dft_avx); } -#endif -TEST(vec_znx_dft, vec_znx_dft) { - test_fft64_vec_znx_dft(vec_znx_dft); -#ifdef __x86_64__ - // FIXME: currently, it only works on avx - test_ntt120_vec_znx_dft(ntt120_vec_znx_dft_avx); -#endif -} - -TEST(vec_dft_add, fft64_vec_dft_add) { test_fft64_vec_dft_add(fft64_vec_dft_add); } - -static void test_fft64_vec_znx_idft(VEC_ZNX_IDFT_F idft, VEC_ZNX_IDFT_TMP_A_F idft_tmp_a, - VEC_ZNX_IDFT_TMP_BYTES_F idft_tmp_bytes) { - for (uint64_t n : {2, 4, 64, 128}) { - MODULE* module = new_module_info(n, FFT64); - uint64_t tmp_size = idft_tmp_bytes ? idft_tmp_bytes(module, n) : 0; - std::vector<uint8_t> tmp(tmp_size); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - fft64_vec_znx_dft_layout a(n, sa); - fft64_vec_znx_big_layout res(n, sr); - a.fill_dft_random_log2bound(22); - std::vector<znx_i64> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - expect[i] = simple_rint_ifft64(a.get_copy_zext(i)); - } - // test the function - if (idft_tmp_bytes) { - thash hash_before = a.content_hash(); - idft(module, res.data, sr, a.data, sa, tmp.data()); - ASSERT_EQ(a.content_hash(), hash_before); - } else { - idft_tmp_a(module, res.data, sr, a.data, sa); - } - for (uint64_t i = 0; i < sr; ++i) { - znx_i64 actual = res.get_copy_zext(i); - // ASSERT_EQ(res.get_copy_zext(i), expect[i]); - if (!(actual == expect[i])) { - for (uint64_t j = 0; j < n; ++j) { - std::cerr << actual.get_coeff(j) << " dft vs. " << expect[i].get_coeff(j) << std::endl; - } - FAIL(); - } - } - } - } - delete_module_info(module); - } -} - -TEST(vec_znx_dft, fft64_vec_znx_idft) { - test_fft64_vec_znx_idft(fft64_vec_znx_idft, nullptr, fft64_vec_znx_idft_tmp_bytes); -} -TEST(vec_znx_dft, fft64_vec_znx_idft_tmp_a) { test_fft64_vec_znx_idft(nullptr, fft64_vec_znx_idft_tmp_a, nullptr); } - -#ifdef __x86_64__ -// FIXME: currently, it only works on avx -static void test_ntt120_vec_znx_idft(VEC_ZNX_IDFT_F idft, VEC_ZNX_IDFT_TMP_A_F idft_tmp_a, - VEC_ZNX_IDFT_TMP_BYTES_F idft_tmp_bytes) { - for (uint64_t n : {2, 4, 64, 128}) { - MODULE* module = new_module_info(n, NTT120); - uint64_t tmp_size = idft_tmp_bytes ? idft_tmp_bytes(module, n) : 0; - std::vector<uint8_t> tmp(tmp_size); - for (uint64_t sa : {3, 5, 8}) { - for (uint64_t sr : {3, 5, 8}) { - ntt120_vec_znx_dft_layout a(n, sa); - ntt120_vec_znx_big_layout res(n, sr); - a.fill_random(); - std::vector<znx_i128> expect(sr); - for (uint64_t i = 0; i < sr; ++i) { - expect[i] = simple_intt120(a.get_copy_zext(i)); - } - // test the function - if (idft_tmp_bytes) { - thash hash_before = a.content_hash(); - idft(module, res.data, sr, a.data, sa, tmp.data()); - ASSERT_EQ(a.content_hash(), hash_before); - } else { - idft_tmp_a(module, res.data, sr, a.data, sa); - } - for (uint64_t i = 0; i < sr; ++i) { - znx_i128 actual = res.get_copy_zext(i); - ASSERT_EQ(res.get_copy_zext(i), expect[i]); - // if (!(actual == expect[i])) { - // for (uint64_t j = 0; j < n; ++j) { - // std::cerr << actual.get_coeff(j) << " dft vs. " << expect[i].get_coeff(j) << std::endl; - // } - // FAIL(); - // } - } - } - } - delete_module_info(module); - } -} - -TEST(vec_znx_dft, ntt120_vec_znx_idft) { - test_ntt120_vec_znx_idft(ntt120_vec_znx_idft_avx, nullptr, ntt120_vec_znx_idft_tmp_bytes_avx); -} -TEST(vec_znx_dft, ntt120_vec_znx_idft_tmp_a) { - test_ntt120_vec_znx_idft(nullptr, ntt120_vec_znx_idft_tmp_a_avx, nullptr); -} -#endif -TEST(vec_znx_dft, vec_znx_idft) { - test_fft64_vec_znx_idft(vec_znx_idft, nullptr, vec_znx_idft_tmp_bytes); -#ifdef __x86_64__ - // FIXME: currently, only supported on avx - test_ntt120_vec_znx_idft(vec_znx_idft, nullptr, vec_znx_idft_tmp_bytes); -#endif -} -TEST(vec_znx_dft, vec_znx_idft_tmp_a) { - test_fft64_vec_znx_idft(nullptr, vec_znx_idft_tmp_a, nullptr); -#ifdef __x86_64__ - // FIXME: currently, only supported on avx - test_ntt120_vec_znx_idft(nullptr, vec_znx_idft_tmp_a, nullptr); -#endif -}
@@ -1,548 +0,0 @@
-#include <cstdint> -#include <utility> - -#include "../spqlios/arithmetic/vec_znx_arithmetic.h" -#include "gtest/gtest.h" -#include "spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "spqlios/coeffs/coeffs_arithmetic.h" -#include "test/testlib/mod_q120.h" -#include "test/testlib/negacyclic_polynomial.h" -#include "testlib/fft64_dft.h" -#include "testlib/polynomial_vector.h" - -TEST(fft64_layouts, dft_idft_fft64) { - uint64_t n = 128; - // create a random polynomial - znx_i64 p(n); - for (uint64_t i = 0; i < n; ++i) { - p.set_coeff(i, uniform_i64_bits(36)); - } - // call fft - reim_fft64vec q = simple_fft64(p); - // call ifft and round - znx_i64 r = simple_rint_ifft64(q); - ASSERT_EQ(p, r); -} - -TEST(znx_layout, valid_test) { - uint64_t n = 4; - znx_vec_i64_layout v(n, 7, 13); - // this should be ok - v.set(0, znx_i64::zero(n)); - // this should be ok - ASSERT_EQ(v.get_copy_zext(0), znx_i64::zero(n)); - ASSERT_EQ(v.data()[2], 0); // should be ok - // this is also ok (zero extended vector) - ASSERT_EQ(v.get_copy_zext(1000), znx_i64::zero(n)); -} - -// disabling this test by default, since it depicts on purpose wrong accesses -#if 0 -TEST(znx_layout, valgrind_antipattern_test) { - uint64_t n = 4; - znx_vec_i64_layout v(n, 7, 13); - // this should be ok - v.set(0, znx_i64::zero(n)); - // this should abort (wrong ring dimension) - ASSERT_DEATH(v.set(3, znx_i64::zero(2 * n)), ""); - // this should abort (out of bounds) - ASSERT_DEATH(v.set(8, znx_i64::zero(n)), ""); - // this should be ok - ASSERT_EQ(v.get_copy_zext(0), znx_i64::zero(n)); - // should be an uninit read - ASSERT_TRUE(!(v.get_copy_zext(2) == znx_i64::zero(n))); // should be uninit - // should be an invalid read (inter-slice) - ASSERT_NE(v.data()[4], 0); - ASSERT_EQ(v.data()[2], 0); // should be ok - // should be an uninit read - ASSERT_NE(v.data()[13], 0); // should be uninit -} -#endif - -// test of binary operations - -// test for out of place calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_binop_outplace(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {2, 4, 8, 128}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {7, 13, 15}) { - for (uint64_t sb : {7, 13, 15}) { - for (uint64_t sc : {7, 13, 15}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - uint64_t c_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - znx_vec_i64_layout lb(n, sb, b_sl); - znx_vec_i64_layout lc(n, sc, c_sl); - std::vector<znx_i64> expect(sc); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sb; ++i) { - lb.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sc; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), lb.get_copy_zext(i)); - } - binop(mod, // N - lc.data(), sc, c_sl, // res - la.data(), sa, a_sl, // a - lb.data(), sb, b_sl); - for (uint64_t i = 0; i < sc; ++i) { - ASSERT_EQ(lc.get_copy_zext(i), expect[i]); - } - } - } - } - delete_module_info(mod); - } -} -// test for inplace1 calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_binop_inplace1(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {2, 4, 64}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {3, 9, 12}) { - for (uint64_t sb : {3, 9, 12}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - znx_vec_i64_layout lb(n, sb, b_sl); - std::vector<znx_i64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sb; ++i) { - lb.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), lb.get_copy_zext(i)); - } - binop(mod, // N - la.data(), sa, a_sl, // res - la.data(), sa, a_sl, // a - lb.data(), sb, b_sl); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]); - } - } - } - delete_module_info(mod); - } -} -// test for inplace2 calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_binop_inplace2(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {4, 32, 64}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {3, 9, 12}) { - for (uint64_t sb : {3, 9, 12}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - znx_vec_i64_layout lb(n, sb, b_sl); - std::vector<znx_i64> expect(sb); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sb; ++i) { - lb.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sb; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), lb.get_copy_zext(i)); - } - binop(mod, // N - lb.data(), sb, b_sl, // res - la.data(), sa, a_sl, // a - lb.data(), sb, b_sl); - for (uint64_t i = 0; i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), expect[i]); - } - } - } - delete_module_info(mod); - } -} -// test for inplace3 calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_binop_inplace3(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - for (uint64_t n : {2, 16, 1024}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {2, 6, 11}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - std::vector<znx_i64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_binop(la.get_copy_zext(i), la.get_copy_zext(i)); - } - binop(mod, // N - la.data(), sa, a_sl, // res - la.data(), sa, a_sl, // a - la.data(), sa, a_sl); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]); - } - } - delete_module_info(mod); - } -} -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_binop(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - test_vec_znx_elemw_binop_outplace(binop, ref_binop); - test_vec_znx_elemw_binop_inplace1(binop, ref_binop); - test_vec_znx_elemw_binop_inplace2(binop, ref_binop); - test_vec_znx_elemw_binop_inplace3(binop, ref_binop); -} - -static znx_i64 poly_add(const znx_i64& a, const znx_i64& b) { return a + b; } -TEST(vec_znx, vec_znx_add) { test_vec_znx_elemw_binop(vec_znx_add, poly_add); } -TEST(vec_znx, vec_znx_add_ref) { test_vec_znx_elemw_binop(vec_znx_add_ref, poly_add); } -#ifdef __x86_64__ -TEST(vec_znx, vec_znx_add_avx) { test_vec_znx_elemw_binop(vec_znx_add_avx, poly_add); } -#endif - -static znx_i64 poly_sub(const znx_i64& a, const znx_i64& b) { return a - b; } -TEST(vec_znx, vec_znx_sub) { test_vec_znx_elemw_binop(vec_znx_sub, poly_sub); } -TEST(vec_znx, vec_znx_sub_ref) { test_vec_znx_elemw_binop(vec_znx_sub_ref, poly_sub); } -#ifdef __x86_64__ -TEST(vec_znx, vec_znx_sub_avx) { test_vec_znx_elemw_binop(vec_znx_sub_avx, poly_sub); } -#endif - -// test of rotation operations - -// test for out of place calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_unop_param_outplace(ACTUAL_FCN test_rotate, EXPECT_FCN ref_rotate, int64_t (*param_gen)()) { - for (uint64_t n : {2, 4, 8, 128}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {7, 13, 15}) { - for (uint64_t sb : {7, 13, 15}) { - { - int64_t p = param_gen(); - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 4 + n; - znx_vec_i64_layout la(n, sa, a_sl); - znx_vec_i64_layout lb(n, sb, b_sl); - std::vector<znx_i64> expect(sb); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sb; ++i) { - expect[i] = ref_rotate(p, la.get_copy_zext(i)); - } - test_rotate(mod, // - p, // - lb.data(), sb, b_sl, // - la.data(), sa, a_sl // - ); - for (uint64_t i = 0; i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), expect[i]) << n << " " << sa << " " << sb << " " << i; - } - } - } - } - delete_module_info(mod); - } -} - -// test for inplace calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_unop_param_inplace(ACTUAL_FCN test_rotate, EXPECT_FCN ref_rotate, int64_t (*param_gen)()) { - for (uint64_t n : {2, 16, 1024}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {2, 6, 11}) { - { - int64_t p = param_gen(); - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - std::vector<znx_i64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_rotate(p, la.get_copy_zext(i)); - } - test_rotate(mod, // N - p, //; - la.data(), sa, a_sl, // res - la.data(), sa, a_sl // a - ); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]) << n << " " << sa << " " << i; - } - } - } - delete_module_info(mod); - } -} - -static int64_t random_rotate_param() { return uniform_i64(); } - -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_rotate(ACTUAL_FCN binop, EXPECT_FCN ref_binop) { - test_vec_znx_elemw_unop_param_outplace(binop, ref_binop, random_rotate_param); - test_vec_znx_elemw_unop_param_inplace(binop, ref_binop, random_rotate_param); -} - -static znx_i64 poly_rotate(const int64_t p, const znx_i64& a) { - uint64_t n = a.nn(); - znx_i64 res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, a.get_coeff(i - p)); - } - return res; -} -TEST(vec_znx, vec_znx_rotate) { test_vec_znx_elemw_rotate(vec_znx_rotate, poly_rotate); } -TEST(vec_znx, vec_znx_rotate_ref) { test_vec_znx_elemw_rotate(vec_znx_rotate_ref, poly_rotate); } - -static int64_t random_automorphism_param() { return uniform_i64() | 1; } - -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_automorphism(ACTUAL_FCN unop, EXPECT_FCN ref_unop) { - test_vec_znx_elemw_unop_param_outplace(unop, ref_unop, random_automorphism_param); - test_vec_znx_elemw_unop_param_inplace(unop, ref_unop, random_automorphism_param); -} - -static znx_i64 poly_automorphism(const int64_t p, const znx_i64& a) { - uint64_t n = a.nn(); - znx_i64 res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i * p, a.get_coeff(i)); - } - return res; -} - -TEST(vec_znx, vec_znx_automorphism) { test_vec_znx_elemw_automorphism(vec_znx_automorphism, poly_automorphism); } -TEST(vec_znx, vec_znx_automorphism_ref) { - test_vec_znx_elemw_automorphism(vec_znx_automorphism_ref, poly_automorphism); -} - -// test for out of place calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_unop_outplace(ACTUAL_FCN test_unop, EXPECT_FCN ref_unop) { - for (uint64_t n : {2, 4, 8, 128}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {7, 13, 15}) { - for (uint64_t sb : {7, 13, 15}) { - { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - uint64_t b_sl = uniform_u64_bits(3) * 4 + n; - znx_vec_i64_layout la(n, sa, a_sl); - znx_vec_i64_layout lb(n, sb, b_sl); - std::vector<znx_i64> expect(sb); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sb; ++i) { - expect[i] = ref_unop(la.get_copy_zext(i)); - } - test_unop(mod, // - lb.data(), sb, b_sl, // - la.data(), sa, a_sl // - ); - for (uint64_t i = 0; i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), expect[i]) << n << " " << sa << " " << sb << " " << i; - } - } - } - } - delete_module_info(mod); - } -} - -// test for inplace calls -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_unop_inplace(ACTUAL_FCN test_unop, EXPECT_FCN ref_unop) { - for (uint64_t n : {2, 16, 1024}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {2, 6, 11}) { - { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - std::vector<znx_i64> expect(sa); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - for (uint64_t i = 0; i < sa; ++i) { - expect[i] = ref_unop(la.get_copy_zext(i)); - } - test_unop(mod, // N - la.data(), sa, a_sl, // res - la.data(), sa, a_sl // a - ); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), expect[i]) << n << " " << sa << " " << i; - } - } - } - delete_module_info(mod); - } -} - -template <typename ACTUAL_FCN, typename EXPECT_FCN> -void test_vec_znx_elemw_unop(ACTUAL_FCN unop, EXPECT_FCN ref_unop) { - test_vec_znx_elemw_unop_outplace(unop, ref_unop); - test_vec_znx_elemw_unop_inplace(unop, ref_unop); -} - -static znx_i64 poly_copy(const znx_i64& a) { return a; } - -TEST(vec_znx, vec_znx_copy) { test_vec_znx_elemw_unop(vec_znx_copy, poly_copy); } -TEST(vec_znx, vec_znx_copy_ref) { test_vec_znx_elemw_unop(vec_znx_copy_ref, poly_copy); } - -static znx_i64 poly_negate(const znx_i64& a) { return -a; } - -TEST(vec_znx, vec_znx_negate) { test_vec_znx_elemw_unop(vec_znx_negate, poly_negate); } -TEST(vec_znx, vec_znx_negate_ref) { test_vec_znx_elemw_unop(vec_znx_negate_ref, poly_negate); } -#ifdef __x86_64__ -TEST(vec_znx, vec_znx_negate_avx) { test_vec_znx_elemw_unop(vec_znx_negate_avx, poly_negate); } -#endif - -static void test_vec_znx_zero(VEC_ZNX_ZERO_F zero) { - for (uint64_t n : {2, 16, 1024}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {2, 6, 11}) { - { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - zero(mod, // N - la.data(), sa, a_sl // res - ); - znx_i64 ZERO = znx_i64::zero(n); - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), ZERO) << n << " " << sa << " " << i; - } - } - } - delete_module_info(mod); - } -} - -TEST(vec_znx, vec_znx_zero) { test_vec_znx_zero(vec_znx_zero); } -TEST(vec_znx, vec_znx_zero_ref) { test_vec_znx_zero(vec_znx_zero_ref); } - -static void vec_poly_normalize(const uint64_t base_k, std::vector<znx_i64>& in) { - if (in.size() > 0) { - uint64_t n = in.front().nn(); - - znx_i64 out = znx_i64::random_log2bound(n, 62); - znx_i64 cinout(n); - for (int64_t i = in.size() - 1; i >= 0; --i) { - znx_normalize(n, base_k, in[i].data(), cinout.data(), in[i].data(), cinout.data()); - } - } -} - -template <typename ACTUAL_FCN, typename TMP_BYTES_FNC> -void test_vec_znx_normalize_outplace(ACTUAL_FCN test_normalize, TMP_BYTES_FNC tmp_bytes) { - for (uint64_t n : {2, 16, 1024}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {1, 2, 6, 11}) { - for (uint64_t sb : {1, 2, 6, 11}) { - for (uint64_t base_k : {19}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - - std::vector<znx_i64> la_norm; - for (uint64_t i = 0; i < sa; ++i) { - la_norm.push_back(la.get_copy_zext(i)); - } - vec_poly_normalize(base_k, la_norm); - - uint64_t b_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout lb(n, sb, b_sl); - - const uint64_t tmp_size = tmp_bytes(mod, n); - uint8_t* tmp = new uint8_t[tmp_size]; - test_normalize(mod, - n, // N - base_k, // base_k - lb.data(), sb, b_sl, // res - la.data(), sa, a_sl, // a - tmp); - delete[] tmp; - - for (uint64_t i = 0; i < std::min(sa, sb); ++i) { - ASSERT_EQ(lb.get_copy_zext(i), la_norm[i]) << n << " " << sa << " " << sb << " " << i; - } - znx_i64 zero(n); - for (uint64_t i = std::min(sa, sb); i < sb; ++i) { - ASSERT_EQ(lb.get_copy_zext(i), zero) << n << " " << sa << " " << sb << " " << i; - } - } - } - } - delete_module_info(mod); - } -} - -TEST(vec_znx, vec_znx_normalize_outplace) { - test_vec_znx_normalize_outplace(vec_znx_normalize_base2k, vec_znx_normalize_base2k_tmp_bytes); -} -TEST(vec_znx, vec_znx_normalize_outplace_ref) { - test_vec_znx_normalize_outplace(vec_znx_normalize_base2k_ref, vec_znx_normalize_base2k_tmp_bytes_ref); -} - -template <typename ACTUAL_FCN, typename TMP_BYTES_FNC> -void test_vec_znx_normalize_inplace(ACTUAL_FCN test_normalize, TMP_BYTES_FNC tmp_bytes) { - for (uint64_t n : {2, 16, 1024}) { - MODULE_TYPE mtype = uniform_u64() % 2 == 0 ? FFT64 : NTT120; - MODULE* mod = new_module_info(n, mtype); - for (uint64_t sa : {2, 6, 11}) { - for (uint64_t base_k : {19}) { - uint64_t a_sl = uniform_u64_bits(3) * 5 + n; - znx_vec_i64_layout la(n, sa, a_sl); - for (uint64_t i = 0; i < sa; ++i) { - la.set(i, znx_i64::random_log2bound(n, 62)); - } - - std::vector<znx_i64> la_norm; - for (uint64_t i = 0; i < sa; ++i) { - la_norm.push_back(la.get_copy_zext(i)); - } - vec_poly_normalize(base_k, la_norm); - - const uint64_t tmp_size = tmp_bytes(mod, n); - uint8_t* tmp = new uint8_t[tmp_size]; - test_normalize(mod, - n, // N - base_k, // base_k - la.data(), sa, a_sl, // res - la.data(), sa, a_sl, // a - tmp); - delete[] tmp; - for (uint64_t i = 0; i < sa; ++i) { - ASSERT_EQ(la.get_copy_zext(i), la_norm[i]) << n << " " << sa << " " << i; - } - } - } - delete_module_info(mod); - } -} - -TEST(vec_znx, vec_znx_normalize_inplace) { - test_vec_znx_normalize_inplace(vec_znx_normalize_base2k, vec_znx_normalize_base2k_tmp_bytes); -} -TEST(vec_znx, vec_znx_normalize_inplace_ref) { - test_vec_znx_normalize_inplace(vec_znx_normalize_base2k_ref, vec_znx_normalize_base2k_tmp_bytes_ref); -}
@@ -1,180 +0,0 @@
-#include <gtest/gtest.h> - -#include "../spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "../spqlios/reim4/reim4_arithmetic.h" -#include "testlib/fft64_layouts.h" -#include "testlib/polynomial_vector.h" - -static void test_vmp_prepare_contiguous(VMP_PREPARE_CONTIGUOUS_F* prepare_contiguous, - VMP_PREPARE_TMP_BYTES_F* tmp_bytes) { - // tests when n < 8 - for (uint64_t nn : {2, 4}) { - MODULE* module = new_module_info(nn, FFT64); - for (uint64_t nrows : {1, 2, 5}) { - for (uint64_t ncols : {2, 6, 7}) { - znx_vec_i64_layout mat(nn, nrows * ncols, nn); - fft64_vmp_pmat_layout pmat(nn, nrows, ncols); - mat.fill_random(30); - std::vector<uint8_t> tmp_space(fft64_vmp_prepare_tmp_bytes(module, nn, nrows, ncols)); - thash hash_before = mat.content_hash(); - prepare_contiguous(module, pmat.data, mat.data(), nrows, ncols, tmp_space.data()); - ASSERT_EQ(mat.content_hash(), hash_before); - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - const double* pmatv = (double*)pmat.data + (col * nrows + row) * nn; - reim_fft64vec tmp = simple_fft64(mat.get_copy(row * ncols + col)); - const double* tmpv = tmp.data(); - for (uint64_t i = 0; i < nn; ++i) { - ASSERT_LE(abs(pmatv[i] - tmpv[i]), 1e-10); - } - } - } - } - } - delete_module_info(module); - } - // tests when n >= 8 - for (uint64_t nn : {8, 32}) { - MODULE* module = new_module_info(nn, FFT64); - uint64_t nblk = nn / 8; - for (uint64_t nrows : {1, 2, 5}) { - for (uint64_t ncols : {2, 6, 7}) { - znx_vec_i64_layout mat(nn, nrows * ncols, nn); - fft64_vmp_pmat_layout pmat(nn, nrows, ncols); - mat.fill_random(30); - std::vector<uint8_t> tmp_space(tmp_bytes(module, nn, nrows, ncols)); - thash hash_before = mat.content_hash(); - prepare_contiguous(module, pmat.data, mat.data(), nrows, ncols, tmp_space.data()); - ASSERT_EQ(mat.content_hash(), hash_before); - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - reim_fft64vec tmp = simple_fft64(mat.get_copy(row * ncols + col)); - for (uint64_t blk = 0; blk < nblk; ++blk) { - reim4_elem expect = tmp.get_blk(blk); - reim4_elem actual = pmat.get(row, col, blk); - ASSERT_LE(infty_dist(actual, expect), 1e-10); - } - } - } - } - } - delete_module_info(module); - } -} - -TEST(vec_znx, vmp_prepare_contiguous) { test_vmp_prepare_contiguous(vmp_prepare_contiguous, vmp_prepare_tmp_bytes); } -TEST(vec_znx, fft64_vmp_prepare_contiguous_ref) { - test_vmp_prepare_contiguous(fft64_vmp_prepare_contiguous_ref, fft64_vmp_prepare_tmp_bytes); -} -#ifdef __x86_64__ -TEST(vec_znx, fft64_vmp_prepare_contiguous_avx) { - test_vmp_prepare_contiguous(fft64_vmp_prepare_contiguous_avx, fft64_vmp_prepare_tmp_bytes); -} -#endif - -static void test_vmp_apply_add(VMP_APPLY_DFT_TO_DFT_ADD_F* apply, VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* tmp_bytes) { - for (uint64_t nn : {32}) { - MODULE* module = new_module_info(nn, FFT64); - for (uint64_t mat_nrows : {1, 4, 7}) { - for (uint64_t mat_ncols : {1, 2, 5}) { - for (uint64_t in_size : {1, 4, 7}) { - for (uint64_t out_size : {1, 2, 5}) { - for (uint64_t mat_scale : {1, 2}) { - fft64_vec_znx_dft_layout in(nn, in_size); - fft64_vmp_pmat_layout pmat(nn, mat_nrows, mat_ncols); - fft64_vec_znx_dft_layout out(nn, out_size); - in.fill_dft_random(0); - pmat.fill_dft_random(0); - out.fill_dft_random(0); - - // naive computation of the product - std::vector<reim_fft64vec> expect(out_size, reim_fft64vec(nn)); - - for (uint64_t col = 0; col < std::min(uint64_t(0), out_size - mat_scale); ++col) { - reim_fft64vec ex = reim_fft64vec::zero(nn); - for (uint64_t row = 0; row < std::min(mat_nrows, in_size); ++row) { - ex += pmat.get_zext(row, col + mat_scale) * in.get_copy_zext(row); - } - expect[col] = ex; - } - - for (uint64_t col = 0; col < out_size; ++col) { - expect[col] += out.get_copy_zext(col); - } - - // apply the product - std::vector<uint8_t> tmp(tmp_bytes(module, nn, out_size, in_size, mat_nrows, mat_ncols)); - apply(module, out.data, out_size, in.data, in_size, pmat.data, mat_nrows, mat_ncols, mat_scale, - tmp.data()); - // check that the output is close from the expectation - for (uint64_t col = 0; col < std::min(uint64_t(0), std::min(mat_ncols, out_size) - mat_scale); ++col) { - reim_fft64vec actual = out.get_copy_zext(col); - ASSERT_LE(infty_dist(actual, expect[col]), 1e-10); - } - } - } - } - } - } - delete_module_info(module); - } -} - -TEST(vec_znx, vmp_apply_to_dft_add) { test_vmp_apply_add(vmp_apply_dft_to_dft_add, vmp_apply_dft_to_dft_tmp_bytes); } -TEST(vec_znx, fft64_vmp_apply_dft_to_dft_add_ref) { - test_vmp_apply_add(fft64_vmp_apply_dft_to_dft_add_ref, vmp_apply_dft_to_dft_tmp_bytes); -} -#ifdef __x86_64__ -TEST(vec_znx, fft64_vmp_apply_dft_to_dft_add_avx) { - test_vmp_apply_add(fft64_vmp_apply_dft_to_dft_add_avx, vmp_apply_dft_to_dft_tmp_bytes); -} -#endif - -static void test_vmp_apply(VMP_APPLY_DFT_TO_DFT_F* apply, VMP_APPLY_DFT_TO_DFT_TMP_BYTES_F* tmp_bytes) { - for (uint64_t nn : {2, 4, 8, 64}) { - MODULE* module = new_module_info(nn, FFT64); - for (uint64_t mat_nrows : {1, 4, 7}) { - for (uint64_t mat_ncols : {1, 2, 5}) { - for (uint64_t in_size : {1, 4, 7}) { - for (uint64_t out_size : {1, 2, 5}) { - fft64_vec_znx_dft_layout in(nn, in_size); - fft64_vmp_pmat_layout pmat(nn, mat_nrows, mat_ncols); - fft64_vec_znx_dft_layout out(nn, out_size); - in.fill_dft_random(0); - pmat.fill_dft_random(0); - - // naive computation of the product - std::vector<reim_fft64vec> expect(out_size, reim_fft64vec(nn)); - for (uint64_t col = 0; col < out_size; ++col) { - reim_fft64vec ex = reim_fft64vec::zero(nn); - for (uint64_t row = 0; row < std::min(mat_nrows, in_size); ++row) { - ex += pmat.get_zext(row, col) * in.get_copy_zext(row); - } - expect[col] = ex; - } - - // apply the product - std::vector<uint8_t> tmp(tmp_bytes(module, nn, out_size, in_size, mat_nrows, mat_ncols)); - apply(module, out.data, out_size, in.data, in_size, pmat.data, mat_nrows, mat_ncols, tmp.data()); - // check that the output is close from the expectation - for (uint64_t col = 0; col < std::min(mat_ncols, out_size); ++col) { - reim_fft64vec actual = out.get_copy_zext(col); - ASSERT_LE(infty_dist(actual, expect[col]), 1e-10); - } - } - } - } - } - delete_module_info(module); - } -} - -TEST(vec_znx, vmp_apply_to_dft) { test_vmp_apply(vmp_apply_dft_to_dft, vmp_apply_dft_to_dft_tmp_bytes); } -TEST(vec_znx, fft64_vmp_apply_dft_to_dft_ref) { - test_vmp_apply(fft64_vmp_apply_dft_to_dft_ref, vmp_apply_dft_to_dft_tmp_bytes); -} -#ifdef __x86_64__ -TEST(vec_znx, fft64_vmp_apply_dft_to_dft_avx) { - test_vmp_apply(fft64_vmp_apply_dft_to_dft_avx, fft64_vmp_apply_dft_to_dft_tmp_bytes); -} -#endif
@@ -1,46 +0,0 @@
-#include "gtest/gtest.h" -#include "spqlios/arithmetic/zn_arithmetic_private.h" -#include "testlib/test_commons.h" - -template <typename INTTYPE> -static void test_tndbl_approxdecomp( // - void (*approxdec)(const MOD_Z*, const TNDBL_APPROXDECOMP_GADGET*, INTTYPE*, uint64_t, const double*, uint64_t) // -) { - for (const uint64_t nn : {1, 3, 8, 51}) { - MOD_Z* module = new_z_module_info(DEFAULT); - for (const uint64_t ell : {1, 2, 7}) { - for (const uint64_t k : {2, 5}) { - TNDBL_APPROXDECOMP_GADGET* gadget = new_tndbl_approxdecomp_gadget(module, k, ell); - for (const uint64_t res_size : {ell * nn}) { - std::vector<double> in(nn); - std::vector<INTTYPE> out(res_size); - for (double& x : in) x = uniform_f64_bounds(-10, 10); - approxdec(module, gadget, out.data(), res_size, in.data(), nn); - // reconstruct the output - double err_bnd = pow(2., -double(ell * k) - 1); - for (uint64_t j = 0; j < nn; ++j) { - double in_j = in[j]; - double out_j = 0; - for (uint64_t i = 0; i < ell; ++i) { - out_j += out[ell * j + i] * pow(2., -double((i + 1) * k)); - } - double err = out_j - in_j; - double err_abs = fabs(err - rint(err)); - ASSERT_LE(err_abs, err_bnd); - } - } - delete_tndbl_approxdecomp_gadget(gadget); - } - } - delete_z_module_info(module); - } -} - -TEST(vec_rnx, i8_tndbl_rnx_approxdecomp) { test_tndbl_approxdecomp(i8_approxdecomp_from_tndbl); } -TEST(vec_rnx, default_i8_tndbl_rnx_approxdecomp) { test_tndbl_approxdecomp(default_i8_approxdecomp_from_tndbl_ref); } - -TEST(vec_rnx, i16_tndbl_rnx_approxdecomp) { test_tndbl_approxdecomp(i16_approxdecomp_from_tndbl); } -TEST(vec_rnx, default_i16_tndbl_rnx_approxdecomp) { test_tndbl_approxdecomp(default_i16_approxdecomp_from_tndbl_ref); } - -TEST(vec_rnx, i32_tndbl_rnx_approxdecomp) { test_tndbl_approxdecomp(i32_approxdecomp_from_tndbl); } -TEST(vec_rnx, default_i32_tndbl_rnx_approxdecomp) { test_tndbl_approxdecomp(default_i32_approxdecomp_from_tndbl_ref); }
@@ -1,104 +0,0 @@
-#include <gtest/gtest.h> -#include <spqlios/arithmetic/zn_arithmetic_private.h> - -#include "testlib/test_commons.h" - -template <typename SRC_T, typename DST_T> -static void test_conv(void (*conv_f)(const MOD_Z*, DST_T* res, uint64_t res_size, const SRC_T* a, uint64_t a_size), - DST_T (*ideal_conv_f)(SRC_T x), SRC_T (*random_f)()) { - MOD_Z* module = new_z_module_info(DEFAULT); - for (uint64_t a_size : {0, 1, 2, 42}) { - for (uint64_t res_size : {0, 1, 2, 42}) { - for (uint64_t trials = 0; trials < 100; ++trials) { - std::vector<SRC_T> a(a_size); - std::vector<DST_T> res(res_size); - uint64_t msize = std::min(a_size, res_size); - for (SRC_T& x : a) x = random_f(); - conv_f(module, res.data(), res_size, a.data(), a_size); - for (uint64_t i = 0; i < msize; ++i) { - DST_T expect = ideal_conv_f(a[i]); - DST_T actual = res[i]; - ASSERT_EQ(expect, actual); - } - for (uint64_t i = msize; i < res_size; ++i) { - DST_T expect = 0; - SRC_T actual = res[i]; - ASSERT_EQ(expect, actual); - } - } - } - } - delete_z_module_info(module); -} - -static int32_t ideal_dbl_to_tn32(double a) { - double _2p32 = INT64_C(1) << 32; - double a_mod_1 = a - rint(a); - int64_t t = rint(a_mod_1 * _2p32); - return int32_t(t); -} - -static double random_f64_10() { return uniform_f64_bounds(-10, 10); } - -static void test_dbl_to_tn32(DBL_TO_TN32_F dbl_to_tn32_f) { - test_conv(dbl_to_tn32_f, ideal_dbl_to_tn32, random_f64_10); -} - -TEST(zn_arithmetic, dbl_to_tn32) { test_dbl_to_tn32(dbl_to_tn32); } -TEST(zn_arithmetic, dbl_to_tn32_ref) { test_dbl_to_tn32(dbl_to_tn32_ref); } - -static double ideal_tn32_to_dbl(int32_t a) { - const double _2p32 = INT64_C(1) << 32; - return double(a) / _2p32; -} - -static int32_t random_t32() { return uniform_i64_bits(32); } - -static void test_tn32_to_dbl(TN32_TO_DBL_F tn32_to_dbl_f) { test_conv(tn32_to_dbl_f, ideal_tn32_to_dbl, random_t32); } - -TEST(zn_arithmetic, tn32_to_dbl) { test_tn32_to_dbl(tn32_to_dbl); } -TEST(zn_arithmetic, tn32_to_dbl_ref) { test_tn32_to_dbl(tn32_to_dbl_ref); } - -static int32_t ideal_dbl_round_to_i32(double a) { return int32_t(rint(a)); } - -static double random_dbl_explaw_18() { return uniform_f64_bounds(-1., 1.) * pow(2., uniform_u64_bits(6) % 19); } - -static void test_dbl_round_to_i32(DBL_ROUND_TO_I32_F dbl_round_to_i32_f) { - test_conv(dbl_round_to_i32_f, ideal_dbl_round_to_i32, random_dbl_explaw_18); -} - -TEST(zn_arithmetic, dbl_round_to_i32) { test_dbl_round_to_i32(dbl_round_to_i32); } -TEST(zn_arithmetic, dbl_round_to_i32_ref) { test_dbl_round_to_i32(dbl_round_to_i32_ref); } - -static double ideal_i32_to_dbl(int32_t a) { return double(a); } - -static int32_t random_i32_explaw_18() { return uniform_i64_bits(uniform_u64_bits(6) % 19); } - -static void test_i32_to_dbl(I32_TO_DBL_F i32_to_dbl_f) { - test_conv(i32_to_dbl_f, ideal_i32_to_dbl, random_i32_explaw_18); -} - -TEST(zn_arithmetic, i32_to_dbl) { test_i32_to_dbl(i32_to_dbl); } -TEST(zn_arithmetic, i32_to_dbl_ref) { test_i32_to_dbl(i32_to_dbl_ref); } - -static int64_t ideal_dbl_round_to_i64(double a) { return rint(a); } - -static double random_dbl_explaw_50() { return uniform_f64_bounds(-1., 1.) * pow(2., uniform_u64_bits(7) % 51); } - -static void test_dbl_round_to_i64(DBL_ROUND_TO_I64_F dbl_round_to_i64_f) { - test_conv(dbl_round_to_i64_f, ideal_dbl_round_to_i64, random_dbl_explaw_50); -} - -TEST(zn_arithmetic, dbl_round_to_i64) { test_dbl_round_to_i64(dbl_round_to_i64); } -TEST(zn_arithmetic, dbl_round_to_i64_ref) { test_dbl_round_to_i64(dbl_round_to_i64_ref); } - -static double ideal_i64_to_dbl(int64_t a) { return double(a); } - -static int64_t random_i64_explaw_50() { return uniform_i64_bits(uniform_u64_bits(7) % 51); } - -static void test_i64_to_dbl(I64_TO_DBL_F i64_to_dbl_f) { - test_conv(i64_to_dbl_f, ideal_i64_to_dbl, random_i64_explaw_50); -} - -TEST(zn_arithmetic, i64_to_dbl) { test_i64_to_dbl(i64_to_dbl); } -TEST(zn_arithmetic, i64_to_dbl_ref) { test_i64_to_dbl(i64_to_dbl_ref); }
@@ -1,94 +0,0 @@
-#include "gtest/gtest.h" -#include "spqlios/arithmetic/zn_arithmetic_private.h" -#include "testlib/zn_layouts.h" - -static void test_zn_vmp_prepare(ZN32_VMP_PREPARE_CONTIGUOUS_F prepare_contiguous) { - MOD_Z* module = new_z_module_info(DEFAULT); - for (uint64_t nrows : {1, 2, 5, 15}) { - for (uint64_t ncols : {1, 2, 32, 42, 67}) { - std::vector<int32_t> src(nrows * ncols); - zn32_pmat_layout out(nrows, ncols); - for (int32_t& x : src) x = uniform_i64_bits(32); - prepare_contiguous(module, out.data, src.data(), nrows, ncols); - for (uint64_t i = 0; i < nrows; ++i) { - for (uint64_t j = 0; j < ncols; ++j) { - int32_t in = src[i * ncols + j]; - int32_t actual = out.get(i, j); - ASSERT_EQ(actual, in); - } - } - } - } - delete_z_module_info(module); -} - -TEST(zn, zn32_vmp_prepare_contiguous) { test_zn_vmp_prepare(zn32_vmp_prepare_contiguous); } -TEST(zn, default_zn32_vmp_prepare_contiguous_ref) { test_zn_vmp_prepare(default_zn32_vmp_prepare_contiguous_ref); } - -static void test_zn_vmp_prepare(ZN32_VMP_PREPARE_DBLPTR_F prepare_dblptr) { - MOD_Z* module = new_z_module_info(DEFAULT); - for (uint64_t nrows : {1, 2, 5, 15}) { - for (uint64_t ncols : {1, 2, 32, 42, 67}) { - std::vector<int32_t> src(nrows * ncols); - zn32_pmat_layout out(nrows, ncols); - for (int32_t& x : src) x = uniform_i64_bits(32); - const int32_t** mat_dblptr = (const int32_t**)malloc(nrows * sizeof(int32_t*)); - for (size_t row_i = 0; row_i < nrows; row_i++) { - mat_dblptr[row_i] = &src.data()[row_i * ncols]; - }; - prepare_dblptr(module, out.data, mat_dblptr, nrows, ncols); - for (uint64_t i = 0; i < nrows; ++i) { - for (uint64_t j = 0; j < ncols; ++j) { - int32_t in = src[i * ncols + j]; - int32_t actual = out.get(i, j); - ASSERT_EQ(actual, in); - } - } - } - } - delete_z_module_info(module); -} - -TEST(zn, zn32_vmp_prepare_dblptr) { test_zn_vmp_prepare(zn32_vmp_prepare_dblptr); } -TEST(zn, default_zn32_vmp_prepare_dblptr_ref) { test_zn_vmp_prepare(default_zn32_vmp_prepare_dblptr_ref); } - -template <typename INTTYPE> -static void test_zn_vmp_apply(void (*apply)(const MOD_Z*, int32_t*, uint64_t, const INTTYPE*, uint64_t, - const ZN32_VMP_PMAT*, uint64_t, uint64_t)) { - MOD_Z* module = new_z_module_info(DEFAULT); - for (uint64_t nrows : {1, 2, 5, 15}) { - for (uint64_t ncols : {1, 2, 32, 42, 67}) { - for (uint64_t a_size : {1, 2, 5, 15}) { - for (uint64_t res_size : {1, 2, 32, 42, 67}) { - std::vector<INTTYPE> a(a_size); - zn32_pmat_layout out(nrows, ncols); - std::vector<int32_t> res(res_size); - for (INTTYPE& x : a) x = uniform_i64_bits(32); - out.fill_random(); - std::vector<int32_t> expect = vmp_product(a.data(), a_size, res_size, out); - apply(module, res.data(), res_size, a.data(), a_size, out.data, nrows, ncols); - for (uint64_t i = 0; i < res_size; ++i) { - int32_t exp = expect[i]; - int32_t actual = res[i]; - ASSERT_EQ(actual, exp); - } - } - } - } - } - delete_z_module_info(module); -} - -TEST(zn, zn32_vmp_apply_i32) { test_zn_vmp_apply(zn32_vmp_apply_i32); } -TEST(zn, zn32_vmp_apply_i16) { test_zn_vmp_apply(zn32_vmp_apply_i16); } -TEST(zn, zn32_vmp_apply_i8) { test_zn_vmp_apply(zn32_vmp_apply_i8); } - -TEST(zn, default_zn32_vmp_apply_i32_ref) { test_zn_vmp_apply(default_zn32_vmp_apply_i32_ref); } -TEST(zn, default_zn32_vmp_apply_i16_ref) { test_zn_vmp_apply(default_zn32_vmp_apply_i16_ref); } -TEST(zn, default_zn32_vmp_apply_i8_ref) { test_zn_vmp_apply(default_zn32_vmp_apply_i8_ref); } - -#ifdef __x86_64__ -TEST(zn, default_zn32_vmp_apply_i32_avx) { test_zn_vmp_apply(default_zn32_vmp_apply_i32_avx); } -TEST(zn, default_zn32_vmp_apply_i16_avx) { test_zn_vmp_apply(default_zn32_vmp_apply_i16_avx); } -TEST(zn, default_zn32_vmp_apply_i8_avx) { test_zn_vmp_apply(default_zn32_vmp_apply_i8_avx); } -#endif
@@ -1,26 +0,0 @@
-#include <gtest/gtest.h> - -#include "../spqlios/arithmetic/vec_znx_arithmetic_private.h" -#include "testlib/negacyclic_polynomial.h" - -static void test_znx_small_single_product(ZNX_SMALL_SINGLE_PRODUCT_F product, - ZNX_SMALL_SINGLE_PRODUCT_TMP_BYTES_F product_tmp_bytes) { - for (const uint64_t nn : {2, 4, 8, 64}) { - MODULE* module = new_module_info(nn, FFT64); - znx_i64 a = znx_i64::random_log2bound(nn, 20); - znx_i64 b = znx_i64::random_log2bound(nn, 20); - znx_i64 expect = naive_product(a, b); - znx_i64 actual(nn); - std::vector<uint8_t> tmp(znx_small_single_product_tmp_bytes(module, nn)); - fft64_znx_small_single_product(module, actual.data(), a.data(), b.data(), tmp.data()); - ASSERT_EQ(actual, expect) << actual.get_coeff(0) << " vs. " << expect.get_coeff(0); - delete_module_info(module); - } -} - -TEST(znx_small, fft64_znx_small_single_product) { - test_znx_small_single_product(fft64_znx_small_single_product, fft64_znx_small_single_product_tmp_bytes); -} -TEST(znx_small, znx_small_single_product) { - test_znx_small_single_product(znx_small_single_product, znx_small_single_product_tmp_bytes); -}
@@ -1,168 +0,0 @@
-#include "fft64_dft.h" - -#include <cstring> - -#include "../../spqlios/reim/reim_fft.h" -#include "../../spqlios/reim/reim_fft_internal.h" - -reim_fft64vec::reim_fft64vec(uint64_t n) : v(n, 0) {} -reim4_elem reim_fft64vec::get_blk(uint64_t blk) const { - return reim_view(v.size() / 2, (double*)v.data()).get_blk(blk); -} -double* reim_fft64vec::data() { return v.data(); } -const double* reim_fft64vec::data() const { return v.data(); } -uint64_t reim_fft64vec::nn() const { return v.size(); } -reim_fft64vec::reim_fft64vec(uint64_t n, const double* data) : v(data, data + n) {} -void reim_fft64vec::save_as(double* dest) const { memcpy(dest, v.data(), nn() * sizeof(double)); } -reim_fft64vec reim_fft64vec::zero(uint64_t n) { return reim_fft64vec(n); } -void reim_fft64vec::set_blk(uint64_t blk, const reim4_elem& value) { - reim_view(v.size() / 2, (double*)v.data()).set_blk(blk, value); -} -reim_fft64vec reim_fft64vec::dft_random(uint64_t n, uint64_t log2bound) { - return simple_fft64(znx_i64::random_log2bound(n, log2bound)); -} -reim_fft64vec reim_fft64vec::random(uint64_t n, double log2bound) { - double bound = pow(2., log2bound); - reim_fft64vec res(n); - for (uint64_t i = 0; i < n; ++i) { - res.v[i] = uniform_f64_bounds(-bound, bound); - } - return res; -} - -reim_fft64vec operator+(const reim_fft64vec& a, const reim_fft64vec& b) { - uint64_t nn = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == a.nn(), "ring dimension mismatch"); - reim_fft64vec res(nn); - double* rv = res.data(); - const double* av = a.data(); - const double* bv = b.data(); - for (uint64_t i = 0; i < nn; ++i) { - rv[i] = av[i] + bv[i]; - } - return res; -} -reim_fft64vec operator-(const reim_fft64vec& a, const reim_fft64vec& b) { - uint64_t nn = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == a.nn(), "ring dimension mismatch"); - reim_fft64vec res(nn); - double* rv = res.data(); - const double* av = a.data(); - const double* bv = b.data(); - for (uint64_t i = 0; i < nn; ++i) { - rv[i] = av[i] - bv[i]; - } - return res; -} -reim_fft64vec operator*(const reim_fft64vec& a, const reim_fft64vec& b) { - uint64_t nn = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == a.nn(), "ring dimension mismatch"); - REQUIRE_DRAMATICALLY(nn >= 2, "test not defined for nn=1"); - uint64_t m = nn / 2; - reim_fft64vec res(nn); - double* rv = res.data(); - const double* av = a.data(); - const double* bv = b.data(); - for (uint64_t i = 0; i < m; ++i) { - rv[i] = av[i] * bv[i] - av[m + i] * bv[m + i]; - rv[m + i] = av[i] * bv[m + i] + av[m + i] * bv[i]; - } - return res; -} -reim_fft64vec& operator+=(reim_fft64vec& a, const reim_fft64vec& b) { - uint64_t nn = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == a.nn(), "ring dimension mismatch"); - double* av = a.data(); - const double* bv = b.data(); - for (uint64_t i = 0; i < nn; ++i) { - av[i] = av[i] + bv[i]; - } - return a; -} -reim_fft64vec& operator-=(reim_fft64vec& a, const reim_fft64vec& b) { - uint64_t nn = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == a.nn(), "ring dimension mismatch"); - double* av = a.data(); - const double* bv = b.data(); - for (uint64_t i = 0; i < nn; ++i) { - av[i] = av[i] - bv[i]; - } - return a; -} - -reim_fft64vec simple_fft64(const znx_i64& polynomial) { - const uint64_t nn = polynomial.nn(); - const uint64_t m = nn / 2; - reim_fft64vec res(nn); - double* dat = res.data(); - for (uint64_t i = 0; i < nn; ++i) dat[i] = polynomial.get_coeff(i); - reim_fft_simple(m, dat); - return res; -} - -znx_i64 simple_rint_ifft64(const reim_fft64vec& fftvec) { - const uint64_t nn = fftvec.nn(); - const uint64_t m = nn / 2; - std::vector<double> vv(fftvec.data(), fftvec.data() + nn); - double* v = vv.data(); - reim_ifft_simple(m, v); - znx_i64 res(nn); - for (uint64_t i = 0; i < nn; ++i) { - res.set_coeff(i, rint(v[i] / m)); - } - return res; -} - -rnx_f64 naive_ifft64(const reim_fft64vec& fftvec) { - const uint64_t nn = fftvec.nn(); - const uint64_t m = nn / 2; - std::vector<double> vv(fftvec.data(), fftvec.data() + nn); - double* v = vv.data(); - reim_ifft_simple(m, v); - rnx_f64 res(nn); - for (uint64_t i = 0; i < nn; ++i) { - res.set_coeff(i, v[i] / m); - } - return res; -} -double infty_dist(const reim_fft64vec& a, const reim_fft64vec& b) { - const uint64_t n = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == a.nn(), "dimensions mismatch"); - const double* da = a.data(); - const double* db = b.data(); - double d = 0; - for (uint64_t i = 0; i < n; ++i) { - double di = abs(da[i] - db[i]); - if (di > d) d = di; - } - return d; -} - -reim_fft64vec simple_fft64(const rnx_f64& polynomial) { - const uint64_t nn = polynomial.nn(); - const uint64_t m = nn / 2; - reim_fft64vec res(nn); - double* dat = res.data(); - for (uint64_t i = 0; i < nn; ++i) dat[i] = polynomial.get_coeff(i); - reim_fft_simple(m, dat); - return res; -} - -reim_fft64vec operator*(double coeff, const reim_fft64vec& v) { - const uint64_t nn = v.nn(); - reim_fft64vec res(nn); - double* rr = res.data(); - const double* vv = v.data(); - for (uint64_t i = 0; i < nn; ++i) rr[i] = coeff * vv[i]; - return res; -} - -rnx_f64 simple_ifft64(const reim_fft64vec& v) { - const uint64_t nn = v.nn(); - const uint64_t m = nn / 2; - rnx_f64 res(nn); - double* dat = res.data(); - memcpy(dat, v.data(), nn * sizeof(double)); - reim_ifft_simple(m, dat); - return res; -}
@@ -1,43 +0,0 @@
-#ifndef SPQLIOS_FFT64_DFT_H -#define SPQLIOS_FFT64_DFT_H - -#include "negacyclic_polynomial.h" -#include "reim4_elem.h" - -class reim_fft64vec { - std::vector<double> v; - - public: - reim_fft64vec() = default; - explicit reim_fft64vec(uint64_t n); - reim_fft64vec(uint64_t n, const double* data); - uint64_t nn() const; - static reim_fft64vec zero(uint64_t n); - /** random complex coefficients (unstructured) */ - static reim_fft64vec random(uint64_t n, double log2bound); - /** random fft of a small int polynomial */ - static reim_fft64vec dft_random(uint64_t n, uint64_t log2bound); - double* data(); - const double* data() const; - void save_as(double* dest) const; - reim4_elem get_blk(uint64_t blk) const; - void set_blk(uint64_t blk, const reim4_elem& value); -}; - -reim_fft64vec operator+(const reim_fft64vec& a, const reim_fft64vec& b); -reim_fft64vec operator-(const reim_fft64vec& a, const reim_fft64vec& b); -reim_fft64vec operator*(const reim_fft64vec& a, const reim_fft64vec& b); -reim_fft64vec operator*(double coeff, const reim_fft64vec& v); -reim_fft64vec& operator+=(reim_fft64vec& a, const reim_fft64vec& b); -reim_fft64vec& operator-=(reim_fft64vec& a, const reim_fft64vec& b); - -/** infty distance */ -double infty_dist(const reim_fft64vec& a, const reim_fft64vec& b); - -reim_fft64vec simple_fft64(const znx_i64& polynomial); -znx_i64 simple_rint_ifft64(const reim_fft64vec& fftvec); -rnx_f64 naive_ifft64(const reim_fft64vec& fftvec); -reim_fft64vec simple_fft64(const rnx_f64& polynomial); -rnx_f64 simple_ifft64(const reim_fft64vec& v); - -#endif // SPQLIOS_FFT64_DFT_H
@@ -1,238 +0,0 @@
-#include "fft64_layouts.h" -#ifdef VALGRIND_MEM_TESTS -#include "valgrind/memcheck.h" -#endif - -void* alloc64(uint64_t size) { - static uint64_t _msk64 = -64; - if (size == 0) return nullptr; - uint64_t rsize = (size + 63) & _msk64; - uint8_t* reps = (uint8_t*)spqlios_alloc(rsize); - REQUIRE_DRAMATICALLY(reps != 0, "Out of memory"); -#ifdef VALGRIND_MEM_TESTS - VALGRIND_MAKE_MEM_NOACCESS(reps + size, rsize - size); -#endif - return reps; -} - -fft64_vec_znx_dft_layout::fft64_vec_znx_dft_layout(uint64_t n, uint64_t size) - : nn(n), // - size(size), // - data((VEC_ZNX_DFT*)alloc64(n * size * 8)), // - view(n / 2, size, (double*)data) {} - -fft64_vec_znx_dft_layout::~fft64_vec_znx_dft_layout() { spqlios_free(data); } - -double* fft64_vec_znx_dft_layout::get_addr(uint64_t idx) { - REQUIRE_DRAMATICALLY(idx < size, "index overflow " << idx << " / " << size); - return ((double*)data) + idx * nn; -} -const double* fft64_vec_znx_dft_layout::get_addr(uint64_t idx) const { - REQUIRE_DRAMATICALLY(idx < size, "index overflow " << idx << " / " << size); - return ((double*)data) + idx * nn; -} -reim_fft64vec fft64_vec_znx_dft_layout::get_copy_zext(uint64_t idx) const { - if (idx < size) { - return reim_fft64vec(nn, get_addr(idx)); - } else { - return reim_fft64vec::zero(nn); - } -} -void fft64_vec_znx_dft_layout::fill_dft_random_log2bound(uint64_t bits) { - for (uint64_t i = 0; i < size; ++i) { - set(i, simple_fft64(znx_i64::random_log2bound(nn, bits))); - } -} -void fft64_vec_znx_dft_layout::set(uint64_t idx, const reim_fft64vec& value) { - REQUIRE_DRAMATICALLY(value.nn() == nn, "ring dimension mismatch"); - value.save_as(get_addr(idx)); -} -thash fft64_vec_znx_dft_layout::content_hash() const { return test_hash(data, size * nn * sizeof(double)); } - -reim4_elem fft64_vec_znx_dft_layout::get(uint64_t idx, uint64_t blk) const { - REQUIRE_DRAMATICALLY(idx < size, "index overflow: " << idx << " / " << size); - REQUIRE_DRAMATICALLY(blk < nn / 8, "blk overflow: " << blk << " / " << nn / 8); - double* reim = ((double*)data) + idx * nn; - return reim4_elem(reim + blk * 4, reim + nn / 2 + blk * 4); -} -reim4_elem fft64_vec_znx_dft_layout::get_zext(uint64_t idx, uint64_t blk) const { - REQUIRE_DRAMATICALLY(blk < nn / 8, "blk overflow: " << blk << " / " << nn / 8); - if (idx < size) { - return get(idx, blk); - } else { - return reim4_elem::zero(); - } -} -void fft64_vec_znx_dft_layout::set(uint64_t idx, uint64_t blk, const reim4_elem& value) { - REQUIRE_DRAMATICALLY(idx < size, "index overflow: " << idx << " / " << size); - REQUIRE_DRAMATICALLY(blk < nn / 8, "blk overflow: " << blk << " / " << nn / 8); - double* reim = ((double*)data) + idx * nn; - value.save_re_im(reim + blk * 4, reim + nn / 2 + blk * 4); -} -void fft64_vec_znx_dft_layout::fill_random(double log2bound) { - for (uint64_t i = 0; i < size; ++i) { - set(i, reim_fft64vec::random(nn, log2bound)); - } -} -void fft64_vec_znx_dft_layout::fill_dft_random(uint64_t log2bound) { - for (uint64_t i = 0; i < size; ++i) { - set(i, reim_fft64vec::dft_random(nn, log2bound)); - } -} - -fft64_vec_znx_big_layout::fft64_vec_znx_big_layout(uint64_t n, uint64_t size) - : nn(n), // - size(size), // - data((VEC_ZNX_BIG*)alloc64(n * size * 8)) {} - -znx_i64 fft64_vec_znx_big_layout::get_copy(uint64_t index) const { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - return znx_i64(nn, ((int64_t*)data) + index * nn); -} -znx_i64 fft64_vec_znx_big_layout::get_copy_zext(uint64_t index) const { - if (index < size) { - return znx_i64(nn, ((int64_t*)data) + index * nn); - } else { - return znx_i64::zero(nn); - } -} -void fft64_vec_znx_big_layout::set(uint64_t index, const znx_i64& value) { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - value.save_as(((int64_t*)data) + index * nn); -} -void fft64_vec_znx_big_layout::fill_random() { - for (uint64_t i = 0; i < size; ++i) { - set(i, znx_i64::random_log2bound(nn, 1)); - } -} -fft64_vec_znx_big_layout::~fft64_vec_znx_big_layout() { spqlios_free(data); } - -fft64_vmp_pmat_layout::fft64_vmp_pmat_layout(uint64_t n, uint64_t nrows, uint64_t ncols) - : nn(n), - nrows(nrows), - ncols(ncols), // - data((VMP_PMAT*)alloc64(nrows * ncols * nn * 8)) {} - -double* fft64_vmp_pmat_layout::get_addr(uint64_t row, uint64_t col, uint64_t blk) const { - REQUIRE_DRAMATICALLY(row < nrows, "row overflow: " << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "col overflow: " << col << " / " << ncols); - REQUIRE_DRAMATICALLY(blk < nn / 8, "block overflow: " << blk << " / " << (nn / 8)); - double* d = (double*)data; - if (col == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - return d + blk * nrows * ncols * 8 // major: blk - + col * nrows * 8 // col == ncols-1 - + row * 8; - } else { - // general case: columns go by pair - return d + blk * nrows * ncols * 8 // major: blk - + (col / 2) * (2 * nrows) * 8 // second: col pair index - + row * 2 * 8 // third: row index - + (col % 2) * 8; // minor: col in colpair - } -} - -reim4_elem fft64_vmp_pmat_layout::get(uint64_t row, uint64_t col, uint64_t blk) const { - return reim4_elem(get_addr(row, col, blk)); -} -reim4_elem fft64_vmp_pmat_layout::get_zext(uint64_t row, uint64_t col, uint64_t blk) const { - REQUIRE_DRAMATICALLY(blk < nn / 8, "block overflow: " << blk << " / " << (nn / 8)); - if (row < nrows && col < ncols) { - return reim4_elem(get_addr(row, col, blk)); - } else { - return reim4_elem::zero(); - } -} -void fft64_vmp_pmat_layout::set(uint64_t row, uint64_t col, uint64_t blk, const reim4_elem& value) const { - value.save_as(get_addr(row, col, blk)); -} - -fft64_vmp_pmat_layout::~fft64_vmp_pmat_layout() { spqlios_free(data); } - -reim_fft64vec fft64_vmp_pmat_layout::get_zext(uint64_t row, uint64_t col) const { - if (row >= nrows || col >= ncols) { - return reim_fft64vec::zero(nn); - } - if (nn < 8) { - // the pmat is just col major - double* addr = (double*)data + (row + col * nrows) * nn; - return reim_fft64vec(nn, addr); - } - // otherwise, reconstruct it block by block - reim_fft64vec res(nn); - for (uint64_t blk = 0; blk < nn / 8; ++blk) { - reim4_elem v = get(row, col, blk); - res.set_blk(blk, v); - } - return res; -} -void fft64_vmp_pmat_layout::set(uint64_t row, uint64_t col, const reim_fft64vec& value) { - REQUIRE_DRAMATICALLY(row < nrows, "row overflow: " << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "row overflow: " << col << " / " << ncols); - if (nn < 8) { - // the pmat is just col major - double* addr = (double*)data + (row + col * nrows) * nn; - value.save_as(addr); - return; - } - // otherwise, reconstruct it block by block - for (uint64_t blk = 0; blk < nn / 8; ++blk) { - reim4_elem v = value.get_blk(blk); - set(row, col, blk, v); - } -} -void fft64_vmp_pmat_layout::fill_random(double log2bound) { - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - set(row, col, reim_fft64vec::random(nn, log2bound)); - } - } -} -void fft64_vmp_pmat_layout::fill_dft_random(uint64_t log2bound) { - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - set(row, col, reim_fft64vec::dft_random(nn, log2bound)); - } - } -} - -fft64_svp_ppol_layout::fft64_svp_ppol_layout(uint64_t n) - : nn(n), // - data((SVP_PPOL*)alloc64(nn * 8)) {} - -reim_fft64vec fft64_svp_ppol_layout::get_copy() const { return reim_fft64vec(nn, (double*)data); } - -void fft64_svp_ppol_layout::set(const reim_fft64vec& value) { value.save_as((double*)data); } - -void fft64_svp_ppol_layout::fill_dft_random(uint64_t log2bound) { set(reim_fft64vec::dft_random(nn, log2bound)); } - -void fft64_svp_ppol_layout::fill_random(double log2bound) { set(reim_fft64vec::random(nn, log2bound)); } - -fft64_svp_ppol_layout::~fft64_svp_ppol_layout() { spqlios_free(data); } -thash fft64_svp_ppol_layout::content_hash() const { return test_hash(data, nn * sizeof(double)); } - -fft64_cnv_left_layout::fft64_cnv_left_layout(uint64_t n, uint64_t size) - : nn(n), // - size(size), - data((CNV_PVEC_L*)alloc64(size * nn * 8)) {} - -reim4_elem fft64_cnv_left_layout::get(uint64_t idx, uint64_t blk) { - REQUIRE_DRAMATICALLY(idx < size, "idx overflow: " << idx << " / " << size); - REQUIRE_DRAMATICALLY(blk < nn / 8, "block overflow: " << blk << " / " << (nn / 8)); - return reim4_elem(((double*)data) + blk * size + idx); -} - -fft64_cnv_left_layout::~fft64_cnv_left_layout() { spqlios_free(data); } - -fft64_cnv_right_layout::fft64_cnv_right_layout(uint64_t n, uint64_t size) - : nn(n), // - size(size), - data((CNV_PVEC_R*)alloc64(size * nn * 8)) {} - -reim4_elem fft64_cnv_right_layout::get(uint64_t idx, uint64_t blk) { - REQUIRE_DRAMATICALLY(idx < size, "idx overflow: " << idx << " / " << size); - REQUIRE_DRAMATICALLY(blk < nn / 8, "block overflow: " << blk << " / " << (nn / 8)); - return reim4_elem(((double*)data) + blk * size + idx); -} - -fft64_cnv_right_layout::~fft64_cnv_right_layout() { spqlios_free(data); }
@@ -1,109 +0,0 @@
-#ifndef SPQLIOS_FFT64_LAYOUTS_H -#define SPQLIOS_FFT64_LAYOUTS_H - -#include "../../spqlios/arithmetic/vec_znx_arithmetic.h" -#include "fft64_dft.h" -#include "negacyclic_polynomial.h" -#include "reim4_elem.h" - -/** @brief test layout for the VEC_ZNX_DFT */ -struct fft64_vec_znx_dft_layout { - public: - const uint64_t nn; - const uint64_t size; - VEC_ZNX_DFT* const data; - reim_vector_view view; - /** @brief fill with random double values (unstructured) */ - void fill_random(double log2bound); - /** @brief fill with random ffts of small int polynomials */ - void fill_dft_random(uint64_t log2bound); - reim4_elem get(uint64_t idx, uint64_t blk) const; - reim4_elem get_zext(uint64_t idx, uint64_t blk) const; - void set(uint64_t idx, uint64_t blk, const reim4_elem& value); - fft64_vec_znx_dft_layout(uint64_t n, uint64_t size); - void fill_random_log2bound(uint64_t bits); - void fill_dft_random_log2bound(uint64_t bits); - double* get_addr(uint64_t idx); - const double* get_addr(uint64_t idx) const; - reim_fft64vec get_copy_zext(uint64_t idx) const; - void set(uint64_t idx, const reim_fft64vec& value); - thash content_hash() const; - ~fft64_vec_znx_dft_layout(); -}; - -/** @brief test layout for the VEC_ZNX_BIG */ -class fft64_vec_znx_big_layout { - public: - const uint64_t nn; - const uint64_t size; - VEC_ZNX_BIG* const data; - fft64_vec_znx_big_layout(uint64_t n, uint64_t size); - void fill_random(); - znx_i64 get_copy(uint64_t index) const; - znx_i64 get_copy_zext(uint64_t index) const; - void set(uint64_t index, const znx_i64& value); - thash content_hash() const; - ~fft64_vec_znx_big_layout(); -}; - -/** @brief test layout for the VMP_PMAT */ -class fft64_vmp_pmat_layout { - public: - const uint64_t nn; - const uint64_t nrows; - const uint64_t ncols; - VMP_PMAT* const data; - fft64_vmp_pmat_layout(uint64_t n, uint64_t nrows, uint64_t ncols); - double* get_addr(uint64_t row, uint64_t col, uint64_t blk) const; - reim4_elem get(uint64_t row, uint64_t col, uint64_t blk) const; - thash content_hash() const; - reim4_elem get_zext(uint64_t row, uint64_t col, uint64_t blk) const; - reim_fft64vec get_zext(uint64_t row, uint64_t col) const; - void set(uint64_t row, uint64_t col, uint64_t blk, const reim4_elem& v) const; - void set(uint64_t row, uint64_t col, const reim_fft64vec& value); - /** @brief fill with random double values (unstructured) */ - void fill_random(double log2bound); - /** @brief fill with random ffts of small int polynomials */ - void fill_dft_random(uint64_t log2bound); - ~fft64_vmp_pmat_layout(); -}; - -/** @brief test layout for the SVP_PPOL */ -class fft64_svp_ppol_layout { - public: - const uint64_t nn; - SVP_PPOL* const data; - fft64_svp_ppol_layout(uint64_t n); - thash content_hash() const; - reim_fft64vec get_copy() const; - void set(const reim_fft64vec&); - /** @brief fill with random double values (unstructured) */ - void fill_random(double log2bound); - /** @brief fill with random ffts of small int polynomials */ - void fill_dft_random(uint64_t log2bound); - ~fft64_svp_ppol_layout(); -}; - -/** @brief test layout for the CNV_PVEC_L */ -class fft64_cnv_left_layout { - const uint64_t nn; - const uint64_t size; - CNV_PVEC_L* const data; - fft64_cnv_left_layout(uint64_t n, uint64_t size); - reim4_elem get(uint64_t idx, uint64_t blk); - thash content_hash() const; - ~fft64_cnv_left_layout(); -}; - -/** @brief test layout for the CNV_PVEC_R */ -class fft64_cnv_right_layout { - const uint64_t nn; - const uint64_t size; - CNV_PVEC_R* const data; - fft64_cnv_right_layout(uint64_t n, uint64_t size); - reim4_elem get(uint64_t idx, uint64_t blk); - thash content_hash() const; - ~fft64_cnv_right_layout(); -}; - -#endif // SPQLIOS_FFT64_LAYOUTS_H
@@ -1,229 +0,0 @@
-#include "mod_q120.h" - -#include <cstdint> -#include <random> - -int64_t centermod(int64_t v, int64_t q) { - int64_t t = v % q; - if (t >= (q + 1) / 2) return t - q; - if (t < -q / 2) return t + q; - return t; -} - -int64_t centermod(uint64_t v, int64_t q) { - int64_t t = int64_t(v % uint64_t(q)); - if (t >= q / 2) return t - q; - return t; -} - -mod_q120::mod_q120() { - for (uint64_t i = 0; i < 4; ++i) { - a[i] = 0; - } -} - -mod_q120::mod_q120(int64_t a0, int64_t a1, int64_t a2, int64_t a3) { - a[0] = centermod(a0, Qi[0]); - a[1] = centermod(a1, Qi[1]); - a[2] = centermod(a2, Qi[2]); - a[3] = centermod(a3, Qi[3]); -} - -mod_q120 operator+(const mod_q120& x, const mod_q120& y) { - mod_q120 r; - for (uint64_t i = 0; i < 4; ++i) { - r.a[i] = centermod(x.a[i] + y.a[i], mod_q120::Qi[i]); - } - return r; -} - -mod_q120 operator-(const mod_q120& x, const mod_q120& y) { - mod_q120 r; - for (uint64_t i = 0; i < 4; ++i) { - r.a[i] = centermod(x.a[i] - y.a[i], mod_q120::Qi[i]); - } - return r; -} - -mod_q120 operator*(const mod_q120& x, const mod_q120& y) { - mod_q120 r; - for (uint64_t i = 0; i < 4; ++i) { - r.a[i] = centermod(x.a[i] * y.a[i], mod_q120::Qi[i]); - } - return r; -} - -mod_q120& operator+=(mod_q120& x, const mod_q120& y) { - for (uint64_t i = 0; i < 4; ++i) { - x.a[i] = centermod(x.a[i] + y.a[i], mod_q120::Qi[i]); - } - return x; -} - -mod_q120& operator-=(mod_q120& x, const mod_q120& y) { - for (uint64_t i = 0; i < 4; ++i) { - x.a[i] = centermod(x.a[i] - y.a[i], mod_q120::Qi[i]); - } - return x; -} - -mod_q120& operator*=(mod_q120& x, const mod_q120& y) { - for (uint64_t i = 0; i < 4; ++i) { - x.a[i] = centermod(x.a[i] * y.a[i], mod_q120::Qi[i]); - } - return x; -} - -int64_t modq_pow(int64_t x, int32_t k, int64_t q) { - k = (k % (q - 1) + q - 1) % (q - 1); - - int64_t res = 1; - int64_t x_pow = centermod(x, q); - while (k != 0) { - if (k & 1) res = centermod(res * x_pow, q); - x_pow = centermod(x_pow * x_pow, q); - k >>= 1; - } - return res; -} - -mod_q120 pow(const mod_q120& x, int32_t k) { - const int64_t r0 = modq_pow(x.a[0], k, x.Qi[0]); - const int64_t r1 = modq_pow(x.a[1], k, x.Qi[1]); - const int64_t r2 = modq_pow(x.a[2], k, x.Qi[2]); - const int64_t r3 = modq_pow(x.a[3], k, x.Qi[3]); - return mod_q120{r0, r1, r2, r3}; -} - -static int64_t half_modq(int64_t x, int64_t q) { - // q must be odd in this function - if (x % 2 == 0) return x / 2; - return centermod((x + q) / 2, q); -} - -mod_q120 half(const mod_q120& x) { - const int64_t r0 = half_modq(x.a[0], x.Qi[0]); - const int64_t r1 = half_modq(x.a[1], x.Qi[1]); - const int64_t r2 = half_modq(x.a[2], x.Qi[2]); - const int64_t r3 = half_modq(x.a[3], x.Qi[3]); - return mod_q120{r0, r1, r2, r3}; -} - -bool operator==(const mod_q120& x, const mod_q120& y) { - for (uint64_t i = 0; i < 4; ++i) { - if (x.a[i] != y.a[i]) return false; - } - return true; -} - -std::ostream& operator<<(std::ostream& out, const mod_q120& x) { - return out << "q120{" << x.a[0] << "," << x.a[1] << "," << x.a[2] << "," << x.a[3] << "}"; -} - -mod_q120 mod_q120::from_q120a(const void* addr) { - static const uint64_t _2p32 = UINT64_C(1) << 32; - const uint64_t* in = (const uint64_t*)addr; - mod_q120 r; - for (uint64_t i = 0; i < 4; ++i) { - REQUIRE_DRAMATICALLY(in[i] < _2p32, "invalid layout a q120"); - r.a[i] = centermod(in[i], mod_q120::Qi[i]); - } - return r; -} - -mod_q120 mod_q120::from_q120b(const void* addr) { - const uint64_t* in = (const uint64_t*)addr; - mod_q120 r; - for (uint64_t i = 0; i < 4; ++i) { - r.a[i] = centermod(in[i], mod_q120::Qi[i]); - } - return r; -} - -mod_q120 mod_q120::from_q120c(const void* addr) { - // static const uint64_t _mask_2p32 = (uint64_t(1) << 32) - 1; - const uint32_t* in = (const uint32_t*)addr; - mod_q120 r; - for (uint64_t i = 0, k = 0; i < 8; i += 2, ++k) { - const uint64_t q = mod_q120::Qi[k]; - uint64_t u = in[i]; - uint64_t w = in[i + 1]; - REQUIRE_DRAMATICALLY(((u << 32) % q) == (w % q), - "invalid layout q120c: " << u << ".2^32 != " << (w >> 32) << " mod " << q); - r.a[k] = centermod(u, q); - } - return r; -} -__int128_t mod_q120::to_int128() const { - static const __int128_t qm[] = {(__int128_t(Qi[1]) * Qi[2]) * Qi[3], (__int128_t(Qi[0]) * Qi[2]) * Qi[3], - (__int128_t(Qi[0]) * Qi[1]) * Qi[3], (__int128_t(Qi[0]) * Qi[1]) * Qi[2]}; - static const int64_t CRTi[] = {Q1_CRT_CST, Q2_CRT_CST, Q3_CRT_CST, Q4_CRT_CST}; - static const __int128_t q = qm[0] * Qi[0]; - static const __int128_t qs2 = q / 2; - __int128_t res = 0; - for (uint64_t i = 0; i < 4; ++i) { - res += (a[i] * CRTi[i] % Qi[i]) * qm[i]; - } - res = (((res % q) + q + qs2) % q) - qs2; // centermod - return res; -} -void mod_q120::save_as_q120a(void* dest) const { - int64_t* d = (int64_t*)dest; - for (uint64_t i = 0; i < 4; ++i) { - d[i] = a[i] + Qi[i]; - } -} -void mod_q120::save_as_q120b(void* dest) const { - int64_t* d = (int64_t*)dest; - for (uint64_t i = 0; i < 4; ++i) { - d[i] = a[i] + (Qi[i] * (1 + uniform_u64_bits(32))); - } -} -void mod_q120::save_as_q120c(void* dest) const { - int32_t* d = (int32_t*)dest; - for (uint64_t i = 0; i < 4; ++i) { - d[2 * i] = a[i] + 3 * Qi[i]; - d[2 * i + 1] = (uint64_t(d[2 * i]) << 32) % uint64_t(Qi[i]); - } -} - -mod_q120 uniform_q120() { - test_rng& gen = randgen(); - std::uniform_int_distribution<uint64_t> dista(0, mod_q120::Qi[0]); - std::uniform_int_distribution<uint64_t> distb(0, mod_q120::Qi[1]); - std::uniform_int_distribution<uint64_t> distc(0, mod_q120::Qi[2]); - std::uniform_int_distribution<uint64_t> distd(0, mod_q120::Qi[3]); - return mod_q120(dista(gen), distb(gen), distc(gen), distd(gen)); -} - -void uniform_q120a(void* dest) { - uint64_t* res = (uint64_t*)dest; - for (uint64_t i = 0; i < 4; ++i) { - res[i] = uniform_u64_bits(32); - } -} - -void uniform_q120b(void* dest) { - uint64_t* res = (uint64_t*)dest; - for (uint64_t i = 0; i < 4; ++i) { - res[i] = uniform_u64(); - } -} - -void uniform_q120c(void* dest) { - uint32_t* res = (uint32_t*)dest; - static const uint64_t _2p32 = uint64_t(1) << 32; - for (uint64_t i = 0, k = 0; i < 8; i += 2, ++k) { - const uint64_t q = mod_q120::Qi[k]; - const uint64_t z = uniform_u64_bits(32); - const uint64_t z_pow_red = (z << 32) % q; - const uint64_t room = (_2p32 - z_pow_red) / q; - const uint64_t z_pow = z_pow_red + (uniform_u64() % room) * q; - REQUIRE_DRAMATICALLY(z < _2p32, "bug!"); - REQUIRE_DRAMATICALLY(z_pow < _2p32, "bug!"); - REQUIRE_DRAMATICALLY(z_pow % q == (z << 32) % q, "bug!"); - - res[i] = (uint32_t)z; - res[i + 1] = (uint32_t)z_pow; - } -}
@@ -1,49 +0,0 @@
-#ifndef SPQLIOS_MOD_Q120_H -#define SPQLIOS_MOD_Q120_H - -#include <cstdint> - -#include "../../spqlios/q120/q120_common.h" -#include "test_commons.h" - -/** @brief centered modulo q */ -int64_t centermod(int64_t v, int64_t q); -int64_t centermod(uint64_t v, int64_t q); - -/** @brief this class represents an integer mod Q120 */ -class mod_q120 { - public: - static constexpr int64_t Qi[] = {Q1, Q2, Q3, Q4}; - int64_t a[4]; - mod_q120(int64_t a1, int64_t a2, int64_t a3, int64_t a4); - mod_q120(); - __int128_t to_int128() const; - static mod_q120 from_q120a(const void* addr); - static mod_q120 from_q120b(const void* addr); - static mod_q120 from_q120c(const void* addr); - void save_as_q120a(void* dest) const; - void save_as_q120b(void* dest) const; - void save_as_q120c(void* dest) const; -}; - -mod_q120 operator+(const mod_q120& x, const mod_q120& y); -mod_q120 operator-(const mod_q120& x, const mod_q120& y); -mod_q120 operator*(const mod_q120& x, const mod_q120& y); -mod_q120& operator+=(mod_q120& x, const mod_q120& y); -mod_q120& operator-=(mod_q120& x, const mod_q120& y); -mod_q120& operator*=(mod_q120& x, const mod_q120& y); -std::ostream& operator<<(std::ostream& out, const mod_q120& x); -bool operator==(const mod_q120& x, const mod_q120& y); -mod_q120 pow(const mod_q120& x, int32_t k); -mod_q120 half(const mod_q120& x); - -/** @brief a uniformly drawn number mod Q120 */ -mod_q120 uniform_q120(); -/** @brief a uniformly random mod Q120 layout A (4 integers < 2^32) */ -void uniform_q120a(void* dest); -/** @brief a uniformly random mod Q120 layout B (4 integers < 2^64) */ -void uniform_q120b(void* dest); -/** @brief a uniformly random mod Q120 layout C (4 integers repr. x,2^32x) */ -void uniform_q120c(void* dest); - -#endif // SPQLIOS_MOD_Q120_H
@@ -1,18 +0,0 @@
-#include "negacyclic_polynomial_impl.h" - -// explicit instantiation -EXPLICIT_INSTANTIATE_POLYNOMIAL(__int128_t); -EXPLICIT_INSTANTIATE_POLYNOMIAL(int64_t); -EXPLICIT_INSTANTIATE_POLYNOMIAL(double); - -double infty_dist(const rnx_f64& a, const rnx_f64& b) { - const uint64_t nn = a.nn(); - const double* aa = a.data(); - const double* bb = b.data(); - double res = 0.; - for (uint64_t i = 0; i < nn; ++i) { - double d = fabs(aa[i] - bb[i]); - if (d > res) res = d; - } - return res; -}
@@ -1,69 +0,0 @@
-#ifndef SPQLIOS_NEGACYCLIC_POLYNOMIAL_H -#define SPQLIOS_NEGACYCLIC_POLYNOMIAL_H - -#include <cstdint> - -#include "test_commons.h" - -template <typename T> -class polynomial; -typedef polynomial<__int128_t> znx_i128; -typedef polynomial<int64_t> znx_i64; -typedef polynomial<double> rnx_f64; - -template <typename T> -class polynomial { - public: - std::vector<T> coeffs; - /** @brief create a polynomial out of existing coeffs */ - polynomial(uint64_t N, const T* c); - /** @brief zero polynomial of dimension N */ - explicit polynomial(uint64_t N); - /** @brief empty polynomial (dim 0) */ - polynomial(); - - /** @brief ring dimension */ - uint64_t nn() const; - /** @brief special setter (accept any indexes, and does the negacyclic translation) */ - void set_coeff(int64_t i, T v); - /** @brief special getter (accept any indexes, and does the negacyclic translation) */ - T get_coeff(int64_t i) const; - /** @brief returns the coefficient layout */ - T* data(); - /** @brief returns the coefficient layout (const version) */ - const T* data() const; - /** @brief saves to the layout */ - void save_as(T* dest) const; - /** @brief zero */ - static polynomial<T> zero(uint64_t n); - /** @brief random polynomial with coefficients in [-2^log2bounds, 2^log2bounds]*/ - static polynomial<T> random_log2bound(uint64_t n, uint64_t log2bound); - /** @brief random polynomial with coefficients in [-2^log2bounds, 2^log2bounds]*/ - static polynomial<T> random(uint64_t n); - /** @brief random polynomial with coefficient in [lb;ub] */ - static polynomial<T> random_bound(uint64_t n, const T lb, const T ub); -}; - -/** @brief equality operator (used during tests) */ -template <typename T> -bool operator==(const polynomial<T>& a, const polynomial<T>& b); - -/** @brief addition operator (used during tests) */ -template <typename T> -polynomial<T> operator+(const polynomial<T>& a, const polynomial<T>& b); - -/** @brief subtraction operator (used during tests) */ -template <typename T> -polynomial<T> operator-(const polynomial<T>& a, const polynomial<T>& b); - -/** @brief negation operator (used during tests) */ -template <typename T> -polynomial<T> operator-(const polynomial<T>& a); - -template <typename T> -polynomial<T> naive_product(const polynomial<T>& a, const polynomial<T>& b); - -/** @brief distance between two real polynomials (used during tests) */ -double infty_dist(const rnx_f64& a, const rnx_f64& b); - -#endif // SPQLIOS_NEGACYCLIC_POLYNOMIAL_H
@@ -1,247 +0,0 @@
-#ifndef SPQLIOS_NEGACYCLIC_POLYNOMIAL_IMPL_H -#define SPQLIOS_NEGACYCLIC_POLYNOMIAL_IMPL_H - -#include "negacyclic_polynomial.h" - -template <typename T> -polynomial<T>::polynomial(uint64_t N, const T* c) : coeffs(N) { - for (uint64_t i = 0; i < N; ++i) coeffs[i] = c[i]; -} -/** @brief zero polynomial of dimension N */ -template <typename T> -polynomial<T>::polynomial(uint64_t N) : coeffs(N, 0) {} -/** @brief empty polynomial (dim 0) */ -template <typename T> -polynomial<T>::polynomial() {} - -/** @brief ring dimension */ -template <typename T> -uint64_t polynomial<T>::nn() const { - uint64_t n = coeffs.size(); - REQUIRE_DRAMATICALLY(is_pow2(n), "polynomial dim is not a pow of 2"); - return n; -} - -/** @brief special setter (accept any indexes, and does the negacyclic translation) */ -template <typename T> -void polynomial<T>::set_coeff(int64_t i, T v) { - const uint64_t n = nn(); - const uint64_t _2nm = 2 * n - 1; - uint64_t pos = uint64_t(i) & _2nm; - if (pos < n) { - coeffs[pos] = v; - } else { - coeffs[pos - n] = -v; - } -} -/** @brief special getter (accept any indexes, and does the negacyclic translation) */ -template <typename T> -T polynomial<T>::get_coeff(int64_t i) const { - const uint64_t n = nn(); - const uint64_t _2nm = 2 * n - 1; - uint64_t pos = uint64_t(i) & _2nm; - if (pos < n) { - return coeffs[pos]; - } else { - return -coeffs[pos - n]; - } -} -/** @brief returns the coefficient layout */ -template <typename T> -T* polynomial<T>::data() { - return coeffs.data(); -} - -template <typename T> -void polynomial<T>::save_as(T* dest) const { - const uint64_t n = nn(); - for (uint64_t i = 0; i < n; ++i) { - dest[i] = coeffs[i]; - } -} - -/** @brief returns the coefficient layout (const version) */ -template <typename T> -const T* polynomial<T>::data() const { - return coeffs.data(); -} - -/** @brief returns the coefficient layout (const version) */ -template <typename T> -polynomial<T> polynomial<T>::zero(uint64_t n) { - return polynomial<T>(n); -} - -/** @brief equality operator (used during tests) */ -template <typename T> -bool operator==(const polynomial<T>& a, const polynomial<T>& b) { - uint64_t n = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == n, "wrong dimensions"); - for (uint64_t i = 0; i < n; ++i) { - if (a.get_coeff(i) != b.get_coeff(i)) return false; - } - return true; -} - -/** @brief addition operator (used during tests) */ -template <typename T> -polynomial<T> operator+(const polynomial<T>& a, const polynomial<T>& b) { - uint64_t n = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == n, "wrong dimensions"); - polynomial<T> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, a.get_coeff(i) + b.get_coeff(i)); - } - return res; -} - -/** @brief subtraction operator (used during tests) */ -template <typename T> -polynomial<T> operator-(const polynomial<T>& a, const polynomial<T>& b) { - uint64_t n = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == n, "wrong dimensions"); - polynomial<T> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, a.get_coeff(i) - b.get_coeff(i)); - } - return res; -} - -/** @brief subtraction operator (used during tests) */ -template <typename T> -polynomial<T> operator-(const polynomial<T>& a) { - uint64_t n = a.nn(); - polynomial<T> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, -a.get_coeff(i)); - } - return res; -} - -/** @brief random polynomial */ -template <typename T> -polynomial<T> random_polynomial(uint64_t n); - -/** @brief random int64 polynomial */ -template <> -polynomial<int64_t> random_polynomial(uint64_t n) { - polynomial<int64_t> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_i64()); - } - return res; -} - -/** @brief random float64 gaussian polynomial */ -template <> -polynomial<double> random_polynomial(uint64_t n) { - polynomial<double> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, random_f64_gaussian()); - } - return res; -} - -template <typename T> -polynomial<T> random_polynomial_bounds(uint64_t n, const T lb, const T ub); - -/** @brief random int64 polynomial */ -template <> -polynomial<int64_t> random_polynomial_bounds(uint64_t n, const int64_t lb, const int64_t ub) { - polynomial<int64_t> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_i64_bounds(lb, ub)); - } - return res; -} - -/** @brief random float64 gaussian polynomial */ -template <> -polynomial<double> random_polynomial_bounds(uint64_t n, const double lb, const double ub) { - polynomial<double> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_f64_bounds(lb, ub)); - } - return res; -} - -/** @brief random int64 polynomial */ -template <> -polynomial<__int128_t> random_polynomial_bounds(uint64_t n, const __int128_t lb, const __int128_t ub) { - polynomial<__int128_t> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_i128_bounds(lb, ub)); - } - return res; -} - -template <typename T> -polynomial<T> random_polynomial_bits(uint64_t n, const uint64_t bits) { - T b = UINT64_C(1) << bits; - return random_polynomial_bounds(n, -b, b); -} - -template <> -polynomial<int64_t> polynomial<int64_t>::random_log2bound(uint64_t n, uint64_t log2bound) { - polynomial<int64_t> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_i64_bits(log2bound)); - } - return res; -} - -template <> -polynomial<int64_t> polynomial<int64_t>::random(uint64_t n) { - polynomial<int64_t> res(n); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_u64()); - } - return res; -} - -template <> -polynomial<double> polynomial<double>::random_log2bound(uint64_t n, uint64_t log2bound) { - polynomial<double> res(n); - double bound = pow(2., log2bound); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_f64_bounds(-bound, bound)); - } - return res; -} - -template <> -polynomial<double> polynomial<double>::random(uint64_t n) { - polynomial<double> res(n); - double bound = 2.; - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, uniform_f64_bounds(-bound, bound)); - } - return res; -} - -template <typename T> -polynomial<T> naive_product(const polynomial<T>& a, const polynomial<T>& b) { - const int64_t nn = a.nn(); - REQUIRE_DRAMATICALLY(b.nn() == uint64_t(nn), "dimension mismatch!"); - polynomial<T> res(nn); - for (int64_t i = 0; i < nn; ++i) { - T ri = 0; - for (int64_t j = 0; j < nn; ++j) { - ri += a.get_coeff(j) * b.get_coeff(i - j); - } - res.set_coeff(i, ri); - } - return res; -} - -#define EXPLICIT_INSTANTIATE_POLYNOMIAL(TYPE) \ - template class polynomial<TYPE>; \ - template bool operator==(const polynomial<TYPE>& a, const polynomial<TYPE>& b); \ - template polynomial<TYPE> operator+(const polynomial<TYPE>& a, const polynomial<TYPE>& b); \ - template polynomial<TYPE> operator-(const polynomial<TYPE>& a, const polynomial<TYPE>& b); \ - template polynomial<TYPE> operator-(const polynomial<TYPE>& a); \ - template polynomial<TYPE> random_polynomial_bits(uint64_t n, const uint64_t bits); \ - template polynomial<TYPE> naive_product(const polynomial<TYPE>& a, const polynomial<TYPE>& b); \ - // template polynomial<TYPE> random_polynomial(uint64_t n); - -#endif // SPQLIOS_NEGACYCLIC_POLYNOMIAL_IMPL_H
@@ -1,122 +0,0 @@
-#include "ntt120_dft.h" - -#include "mod_q120.h" - -// @brief alternative version of the NTT - -/** for all s=k/2^17, root_of_unity(s) = omega_0^k */ -static mod_q120 root_of_unity(double s) { - static mod_q120 omega_2pow17{OMEGA1, OMEGA2, OMEGA3, OMEGA4}; - static double _2pow17 = 1 << 17; - return pow(omega_2pow17, s * _2pow17); -} -static mod_q120 root_of_unity_inv(double s) { - static mod_q120 omega_2pow17{OMEGA1, OMEGA2, OMEGA3, OMEGA4}; - static double _2pow17 = 1 << 17; - return pow(omega_2pow17, -s * _2pow17); -} - -/** recursive naive ntt */ -static void q120_ntt_naive_rec(uint64_t n, double entry_pwr, mod_q120* data) { - if (n == 1) return; - const uint64_t h = n / 2; - const double s = entry_pwr / 2.; - mod_q120 om = root_of_unity(s); - for (uint64_t j = 0; j < h; ++j) { - mod_q120 om_right = data[h + j] * om; - data[h + j] = data[j] - om_right; - data[j] = data[j] + om_right; - } - q120_ntt_naive_rec(h, s, data); - q120_ntt_naive_rec(h, s + 0.5, data + h); -} -static void q120_intt_naive_rec(uint64_t n, double entry_pwr, mod_q120* data) { - if (n == 1) return; - const uint64_t h = n / 2; - const double s = entry_pwr / 2.; - q120_intt_naive_rec(h, s, data); - q120_intt_naive_rec(h, s + 0.5, data + h); - mod_q120 om = root_of_unity_inv(s); - for (uint64_t j = 0; j < h; ++j) { - mod_q120 dat_diff = half(data[j] - data[h + j]); - data[j] = half(data[j] + data[h + j]); - data[h + j] = dat_diff * om; - } -} - -/** user friendly version */ -q120_nttvec simple_ntt120(const znx_i64& polynomial) { - const uint64_t n = polynomial.nn(); - q120_nttvec res(n); - for (uint64_t i = 0; i < n; ++i) { - int64_t xi = polynomial.get_coeff(i); - res.v[i] = mod_q120(xi, xi, xi, xi); - } - q120_ntt_naive_rec(n, 0.5, res.v.data()); - return res; -} - -znx_i128 simple_intt120(const q120_nttvec& fftvec) { - const uint64_t n = fftvec.nn(); - q120_nttvec copy = fftvec; - znx_i128 res(n); - q120_intt_naive_rec(n, 0.5, copy.v.data()); - for (uint64_t i = 0; i < n; ++i) { - res.set_coeff(i, copy.v[i].to_int128()); - } - return res; -} -bool operator==(const q120_nttvec& a, const q120_nttvec& b) { return a.v == b.v; } - -std::vector<mod_q120> q120_ntt_naive(const std::vector<mod_q120>& x) { - std::vector<mod_q120> res = x; - q120_ntt_naive_rec(res.size(), 0.5, res.data()); - return res; -} -q120_nttvec::q120_nttvec(uint64_t n) : v(n) {} -q120_nttvec::q120_nttvec(uint64_t n, const q120b* data) : v(n) { - int64_t* d = (int64_t*)data; - for (uint64_t i = 0; i < n; ++i) { - v[i] = mod_q120::from_q120b(d + 4 * i); - } -} -q120_nttvec::q120_nttvec(uint64_t n, const q120c* data) : v(n) { - int64_t* d = (int64_t*)data; - for (uint64_t i = 0; i < n; ++i) { - v[i] = mod_q120::from_q120c(d + 4 * i); - } -} -uint64_t q120_nttvec::nn() const { return v.size(); } -q120_nttvec q120_nttvec::zero(uint64_t n) { return q120_nttvec(n); } -void q120_nttvec::save_as(q120a* dest) const { - int64_t* const d = (int64_t*)dest; - const uint64_t n = nn(); - for (uint64_t i = 0; i < n; ++i) { - v[i].save_as_q120a(d + 4 * i); - } -} -void q120_nttvec::save_as(q120b* dest) const { - int64_t* const d = (int64_t*)dest; - const uint64_t n = nn(); - for (uint64_t i = 0; i < n; ++i) { - v[i].save_as_q120b(d + 4 * i); - } -} -void q120_nttvec::save_as(q120c* dest) const { - int64_t* const d = (int64_t*)dest; - const uint64_t n = nn(); - for (uint64_t i = 0; i < n; ++i) { - v[i].save_as_q120c(d + 4 * i); - } -} -mod_q120 q120_nttvec::get_blk(uint64_t blk) const { - REQUIRE_DRAMATICALLY(blk < nn(), "blk overflow"); - return v[blk]; -} -q120_nttvec q120_nttvec::random(uint64_t n) { - q120_nttvec res(n); - for (uint64_t i = 0; i < n; ++i) { - res.v[i] = uniform_q120(); - } - return res; -}
@@ -1,31 +0,0 @@
-#ifndef SPQLIOS_NTT120_DFT_H -#define SPQLIOS_NTT120_DFT_H - -#include <vector> - -#include "../../spqlios/q120/q120_arithmetic.h" -#include "mod_q120.h" -#include "negacyclic_polynomial.h" -#include "test_commons.h" - -class q120_nttvec { - public: - std::vector<mod_q120> v; - q120_nttvec() = default; - explicit q120_nttvec(uint64_t n); - q120_nttvec(uint64_t n, const q120b* data); - q120_nttvec(uint64_t n, const q120c* data); - uint64_t nn() const; - static q120_nttvec zero(uint64_t n); - static q120_nttvec random(uint64_t n); - void save_as(q120a* dest) const; - void save_as(q120b* dest) const; - void save_as(q120c* dest) const; - mod_q120 get_blk(uint64_t blk) const; -}; - -q120_nttvec simple_ntt120(const znx_i64& polynomial); -znx_i128 simple_intt120(const q120_nttvec& fftvec); -bool operator==(const q120_nttvec& a, const q120_nttvec& b); - -#endif // SPQLIOS_NTT120_DFT_H
@@ -1,66 +0,0 @@
-#include "ntt120_layouts.h" - -mod_q120x2::mod_q120x2() {} -mod_q120x2::mod_q120x2(const mod_q120& a, const mod_q120& b) { - value[0] = a; - value[1] = b; -} -mod_q120x2::mod_q120x2(q120x2b* addr) { - uint64_t* p = (uint64_t*)addr; - value[0] = mod_q120::from_q120b(p); - value[1] = mod_q120::from_q120b(p + 4); -} - -ntt120_vec_znx_dft_layout::ntt120_vec_znx_dft_layout(uint64_t n, uint64_t size) - : nn(n), // - size(size), // - data((VEC_ZNX_DFT*)alloc64(n * size * 4 * sizeof(uint64_t))) {} - -mod_q120x2 ntt120_vec_znx_dft_layout::get_copy_zext(uint64_t idx, uint64_t blk) { - return mod_q120x2(get_blk(idx, blk)); -} -q120x2b* ntt120_vec_znx_dft_layout::get_blk(uint64_t idx, uint64_t blk) { - REQUIRE_DRAMATICALLY(idx < size, "idx overflow"); - REQUIRE_DRAMATICALLY(blk < nn / 2, "blk overflow"); - uint64_t* d = (uint64_t*)data; - return (q120x2b*)(d + 4 * nn * idx + 8 * blk); -} -ntt120_vec_znx_dft_layout::~ntt120_vec_znx_dft_layout() { spqlios_free(data); } -q120_nttvec ntt120_vec_znx_dft_layout::get_copy_zext(uint64_t idx) { - int64_t* d = (int64_t*)data; - if (idx < size) { - return q120_nttvec(nn, (q120b*)(d + idx * nn * 4)); - } else { - return q120_nttvec::zero(nn); - } -} -void ntt120_vec_znx_dft_layout::set(uint64_t idx, const q120_nttvec& value) { - REQUIRE_DRAMATICALLY(idx < size, "index overflow: " << idx << " / " << size); - q120b* dest_addr = (q120b*)((int64_t*)data + idx * nn * 4); - value.save_as(dest_addr); -} -void ntt120_vec_znx_dft_layout::fill_random() { - for (uint64_t i = 0; i < size; ++i) { - set(i, q120_nttvec::random(nn)); - } -} -thash ntt120_vec_znx_dft_layout::content_hash() const { return test_hash(data, nn * size * 4 * sizeof(int64_t)); } -ntt120_vec_znx_big_layout::ntt120_vec_znx_big_layout(uint64_t n, uint64_t size) - : nn(n), // - size(size), - data((VEC_ZNX_BIG*)alloc64(n * size * sizeof(__int128_t))) {} - -znx_i128 ntt120_vec_znx_big_layout::get_copy(uint64_t index) const { return znx_i128(nn, get_addr(index)); } -znx_i128 ntt120_vec_znx_big_layout::get_copy_zext(uint64_t index) const { - if (index < size) { - return znx_i128(nn, get_addr(index)); - } else { - return znx_i128::zero(nn); - } -} -__int128* ntt120_vec_znx_big_layout::get_addr(uint64_t index) const { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - return (__int128_t*)data + index * nn; -} -void ntt120_vec_znx_big_layout::set(uint64_t index, const znx_i128& value) { value.save_as(get_addr(index)); } -ntt120_vec_znx_big_layout::~ntt120_vec_znx_big_layout() { spqlios_free(data); }
@@ -1,103 +0,0 @@
-#ifndef SPQLIOS_NTT120_LAYOUTS_H -#define SPQLIOS_NTT120_LAYOUTS_H - -#include "../../spqlios/arithmetic/vec_znx_arithmetic.h" -#include "mod_q120.h" -#include "negacyclic_polynomial.h" -#include "ntt120_dft.h" -#include "test_commons.h" - -struct q120b_vector_view {}; - -struct mod_q120x2 { - mod_q120 value[2]; - mod_q120x2(); - mod_q120x2(const mod_q120& a, const mod_q120& b); - mod_q120x2(__int128_t value); - explicit mod_q120x2(q120x2b* addr); - explicit mod_q120x2(q120x2c* addr); - void save_as(q120x2b* addr) const; - void save_as(q120x2c* addr) const; - static mod_q120x2 random(); -}; -mod_q120x2 operator+(const mod_q120x2& a, const mod_q120x2& b); -mod_q120x2 operator-(const mod_q120x2& a, const mod_q120x2& b); -mod_q120x2 operator*(const mod_q120x2& a, const mod_q120x2& b); -bool operator==(const mod_q120x2& a, const mod_q120x2& b); -bool operator!=(const mod_q120x2& a, const mod_q120x2& b); -mod_q120x2& operator+=(mod_q120x2& a, const mod_q120x2& b); -mod_q120x2& operator-=(mod_q120x2& a, const mod_q120x2& b); - -/** @brief test layout for the VEC_ZNX_DFT */ -struct ntt120_vec_znx_dft_layout { - const uint64_t nn; - const uint64_t size; - VEC_ZNX_DFT* const data; - ntt120_vec_znx_dft_layout(uint64_t n, uint64_t size); - mod_q120x2 get_copy_zext(uint64_t idx, uint64_t blk); - q120_nttvec get_copy_zext(uint64_t idx); - void set(uint64_t idx, const q120_nttvec& v); - q120x2b* get_blk(uint64_t idx, uint64_t blk); - thash content_hash() const; - void fill_random(); - ~ntt120_vec_znx_dft_layout(); -}; - -/** @brief test layout for the VEC_ZNX_BIG */ -class ntt120_vec_znx_big_layout { - public: - const uint64_t nn; - const uint64_t size; - VEC_ZNX_BIG* const data; - ntt120_vec_znx_big_layout(uint64_t n, uint64_t size); - - private: - __int128* get_addr(uint64_t index) const; - - public: - znx_i128 get_copy(uint64_t index) const; - znx_i128 get_copy_zext(uint64_t index) const; - void set(uint64_t index, const znx_i128& value); - ~ntt120_vec_znx_big_layout(); -}; - -/** @brief test layout for the VMP_PMAT */ -class ntt120_vmp_pmat_layout { - const uint64_t nn; - const uint64_t nrows; - const uint64_t ncols; - VMP_PMAT* const data; - ntt120_vmp_pmat_layout(uint64_t n, uint64_t nrows, uint64_t ncols); - mod_q120x2 get(uint64_t row, uint64_t col, uint64_t blk) const; - ~ntt120_vmp_pmat_layout(); -}; - -/** @brief test layout for the SVP_PPOL */ -class ntt120_svp_ppol_layout { - const uint64_t nn; - SVP_PPOL* const data; - ntt120_svp_ppol_layout(uint64_t n); - ~ntt120_svp_ppol_layout(); -}; - -/** @brief test layout for the CNV_PVEC_L */ -class ntt120_cnv_left_layout { - const uint64_t nn; - const uint64_t size; - CNV_PVEC_L* const data; - ntt120_cnv_left_layout(uint64_t n, uint64_t size); - mod_q120x2 get(uint64_t idx, uint64_t blk); - ~ntt120_cnv_left_layout(); -}; - -/** @brief test layout for the CNV_PVEC_R */ -class ntt120_cnv_right_layout { - const uint64_t nn; - const uint64_t size; - CNV_PVEC_R* const data; - ntt120_cnv_right_layout(uint64_t n, uint64_t size); - mod_q120x2 get(uint64_t idx, uint64_t blk); - ~ntt120_cnv_right_layout(); -}; - -#endif // SPQLIOS_NTT120_LAYOUTS_H
@@ -1,69 +0,0 @@
-#include "polynomial_vector.h" - -#include <cstring> - -#ifdef VALGRIND_MEM_TESTS -#include "valgrind/memcheck.h" -#endif - -#define CANARY_PADDING (1024) -#define GARBAGE_VALUE (242) - -znx_vec_i64_layout::znx_vec_i64_layout(uint64_t n, uint64_t size, uint64_t slice) : n(n), size(size), slice(slice) { - REQUIRE_DRAMATICALLY(is_pow2(n), "not a power of 2" << n); - REQUIRE_DRAMATICALLY(slice >= n, "slice too small" << slice << " < " << n); - this->region = (uint8_t*)malloc(size * slice * sizeof(int64_t) + 2 * CANARY_PADDING); - this->data_start = (int64_t*)(region + CANARY_PADDING); - // ensure that any invalid value is kind-of garbage - memset(region, GARBAGE_VALUE, size * slice * sizeof(int64_t) + 2 * CANARY_PADDING); - // mark inter-slice memory as non accessible -#ifdef VALGRIND_MEM_TESTS - VALGRIND_MAKE_MEM_NOACCESS(region, CANARY_PADDING); - VALGRIND_MAKE_MEM_NOACCESS(region + size * slice * sizeof(int64_t) + CANARY_PADDING, CANARY_PADDING); - for (uint64_t i = 0; i < size; ++i) { - VALGRIND_MAKE_MEM_UNDEFINED(data_start + i * slice, n * sizeof(int64_t)); - } - if (size != slice) { - for (uint64_t i = 0; i < size; ++i) { - VALGRIND_MAKE_MEM_NOACCESS(data_start + i * slice + n, (slice - n) * sizeof(int64_t)); - } - } -#endif -} - -znx_vec_i64_layout::~znx_vec_i64_layout() { free(region); } - -znx_i64 znx_vec_i64_layout::get_copy_zext(uint64_t index) const { - if (index < size) { - return znx_i64(n, data_start + index * slice); - } else { - return znx_i64::zero(n); - } -} - -znx_i64 znx_vec_i64_layout::get_copy(uint64_t index) const { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - return znx_i64(n, data_start + index * slice); -} - -void znx_vec_i64_layout::set(uint64_t index, const znx_i64& elem) { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - REQUIRE_DRAMATICALLY(elem.nn() == n, "incompatible ring dimensions: " << elem.nn() << " / " << n); - elem.save_as(data_start + index * slice); -} - -int64_t* znx_vec_i64_layout::data() { return data_start; } -const int64_t* znx_vec_i64_layout::data() const { return data_start; } - -void znx_vec_i64_layout::fill_random(uint64_t bits) { - for (uint64_t i = 0; i < size; ++i) { - set(i, znx_i64::random_log2bound(n, bits)); - } -} -__uint128_t znx_vec_i64_layout::content_hash() const { - test_hasher hasher; - for (uint64_t i = 0; i < size; ++i) { - hasher.update(data() + i * slice, n * sizeof(int64_t)); - } - return hasher.hash(); -}
@@ -1,42 +0,0 @@
-#ifndef SPQLIOS_POLYNOMIAL_VECTOR_H -#define SPQLIOS_POLYNOMIAL_VECTOR_H - -#include "negacyclic_polynomial.h" -#include "test_commons.h" - -/** @brief a test memory layout for znx i64 polynomials vectors */ -class znx_vec_i64_layout { - uint64_t n; - uint64_t size; - uint64_t slice; - int64_t* data_start; - uint8_t* region; - - public: - // NO-COPY structure - znx_vec_i64_layout(const znx_vec_i64_layout&) = delete; - void operator=(const znx_vec_i64_layout&) = delete; - znx_vec_i64_layout(znx_vec_i64_layout&&) = delete; - void operator=(znx_vec_i64_layout&&) = delete; - /** @brief initialises a memory layout */ - znx_vec_i64_layout(uint64_t n, uint64_t size, uint64_t slice); - /** @brief destructor */ - ~znx_vec_i64_layout(); - - /** @brief get a copy of item index index (extended with zeros) */ - znx_i64 get_copy_zext(uint64_t index) const; - /** @brief get a copy of item index index (extended with zeros) */ - znx_i64 get_copy(uint64_t index) const; - /** @brief get a copy of item index index (index<size) */ - void set(uint64_t index, const znx_i64& elem); - /** @brief fill with random values */ - void fill_random(uint64_t bits = 63); - /** @brief raw pointer access */ - int64_t* data(); - /** @brief raw pointer access (const version) */ - const int64_t* data() const; - /** @brief content hashcode */ - __uint128_t content_hash() const; -}; - -#endif // SPQLIOS_POLYNOMIAL_VECTOR_H
@@ -1,53 +0,0 @@
-#include <cstdint> -#include <random> - -#include "test_commons.h" - -bool is_pow2(uint64_t n) { return !(n & (n - 1)); } - -test_rng& randgen() { - static test_rng gen; - return gen; -} -uint64_t uniform_u64() { - static std::uniform_int_distribution<uint64_t> dist64(0, UINT64_MAX); - return dist64(randgen()); -} - -uint64_t uniform_u64_bits(uint64_t nbits) { - if (nbits >= 64) return uniform_u64(); - return uniform_u64() >> (64 - nbits); -} - -int64_t uniform_i64() { - std::uniform_int_distribution<int64_t> dist; - return dist(randgen()); -} - -int64_t uniform_i64_bits(uint64_t nbits) { - int64_t bound = int64_t(1) << nbits; - std::uniform_int_distribution<int64_t> dist(-bound, bound); - return dist(randgen()); -} - -int64_t uniform_i64_bounds(const int64_t lb, const int64_t ub) { - std::uniform_int_distribution<int64_t> dist(lb, ub); - return dist(randgen()); -} - -__int128_t uniform_i128_bounds(const __int128_t lb, const __int128_t ub) { - std::uniform_int_distribution<__int128_t> dist(lb, ub); - return dist(randgen()); -} - -double random_f64_gaussian(double stdev) { - std::normal_distribution<double> dist(0, stdev); - return dist(randgen()); -} - -double uniform_f64_bounds(const double lb, const double ub) { - std::uniform_real_distribution<double> dist(lb, ub); - return dist(randgen()); -} - -double uniform_f64_01() { return uniform_f64_bounds(0, 1); }
@@ -1,145 +0,0 @@
-#include "reim4_elem.h" - -reim4_elem::reim4_elem(const double* re, const double* im) { - for (uint64_t i = 0; i < 4; ++i) { - value[i] = re[i]; - value[4 + i] = im[i]; - } -} -reim4_elem::reim4_elem(const double* layout) { - for (uint64_t i = 0; i < 8; ++i) { - value[i] = layout[i]; - } -} -reim4_elem::reim4_elem() { - for (uint64_t i = 0; i < 8; ++i) { - value[i] = 0.; - } -} -void reim4_elem::save_re_im(double* re, double* im) const { - for (uint64_t i = 0; i < 4; ++i) { - re[i] = value[i]; - im[i] = value[4 + i]; - } -} -void reim4_elem::save_as(double* reim4) const { - for (uint64_t i = 0; i < 8; ++i) { - reim4[i] = value[i]; - } -} -reim4_elem reim4_elem::zero() { return reim4_elem(); } - -bool operator==(const reim4_elem& x, const reim4_elem& y) { - for (uint64_t i = 0; i < 8; ++i) { - if (x.value[i] != y.value[i]) return false; - } - return true; -} - -reim4_elem gaussian_reim4() { - test_rng& gen = randgen(); - std::normal_distribution<double> dist(0, 1); - reim4_elem res; - for (uint64_t i = 0; i < 8; ++i) { - res.value[i] = dist(gen); - } - return res; -} - -reim4_array_view::reim4_array_view(uint64_t size, double* data) : size(size), data(data) {} -reim4_elem reim4_array_view::get(uint64_t i) const { - REQUIRE_DRAMATICALLY(i < size, "reim4 array overflow"); - return reim4_elem(data + 8 * i); -} -void reim4_array_view::set(uint64_t i, const reim4_elem& value) { - REQUIRE_DRAMATICALLY(i < size, "reim4 array overflow"); - value.save_as(data + 8 * i); -} - -reim_view::reim_view(uint64_t m, double* data) : m(m), data(data) {} -reim4_elem reim_view::get_blk(uint64_t i) { - REQUIRE_DRAMATICALLY(i < m / 4, "block overflow"); - return reim4_elem(data + 4 * i, data + m + 4 * i); -} -void reim_view::set_blk(uint64_t i, const reim4_elem& value) { - REQUIRE_DRAMATICALLY(i < m / 4, "block overflow"); - value.save_re_im(data + 4 * i, data + m + 4 * i); -} - -reim_vector_view::reim_vector_view(uint64_t m, uint64_t nrows, double* data) : m(m), nrows(nrows), data(data) {} -reim_view reim_vector_view::row(uint64_t row) { - REQUIRE_DRAMATICALLY(row < nrows, "row overflow"); - return reim_view(m, data + 2 * m * row); -} - -/** @brief addition */ -reim4_elem operator+(const reim4_elem& x, const reim4_elem& y) { - reim4_elem reps; - for (uint64_t i = 0; i < 8; ++i) { - reps.value[i] = x.value[i] + y.value[i]; - } - return reps; -} -reim4_elem& operator+=(reim4_elem& x, const reim4_elem& y) { - for (uint64_t i = 0; i < 8; ++i) { - x.value[i] += y.value[i]; - } - return x; -} -/** @brief subtraction */ -reim4_elem operator-(const reim4_elem& x, const reim4_elem& y) { - reim4_elem reps; - for (uint64_t i = 0; i < 8; ++i) { - reps.value[i] = x.value[i] + y.value[i]; - } - return reps; -} -reim4_elem& operator-=(reim4_elem& x, const reim4_elem& y) { - for (uint64_t i = 0; i < 8; ++i) { - x.value[i] -= y.value[i]; - } - return x; -} -/** @brief product */ -reim4_elem operator*(const reim4_elem& x, const reim4_elem& y) { - reim4_elem reps; - for (uint64_t i = 0; i < 4; ++i) { - double xre = x.value[i]; - double yre = y.value[i]; - double xim = x.value[i + 4]; - double yim = y.value[i + 4]; - reps.value[i] = xre * yre - xim * yim; - reps.value[i + 4] = xre * yim + xim * yre; - } - return reps; -} -/** @brief distance in infty norm */ -double infty_dist(const reim4_elem& x, const reim4_elem& y) { - double dist = 0; - for (uint64_t i = 0; i < 8; ++i) { - double d = fabs(x.value[i] - y.value[i]); - if (d > dist) dist = d; - } - return dist; -} - -std::ostream& operator<<(std::ostream& out, const reim4_elem& x) { - out << "[\n"; - for (uint64_t i = 0; i < 4; ++i) { - out << " re=" << x.value[i] << ", im=" << x.value[i + 4] << "\n"; - } - return out << "]"; -} - -reim4_matrix_view::reim4_matrix_view(uint64_t nrows, uint64_t ncols, double* data) - : nrows(nrows), ncols(ncols), data(data) {} -reim4_elem reim4_matrix_view::get(uint64_t row, uint64_t col) const { - REQUIRE_DRAMATICALLY(row < nrows, "rows out of bounds" << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "cols out of bounds" << col << " / " << ncols); - return reim4_elem(data + 8 * (row * ncols + col)); -} -void reim4_matrix_view::set(uint64_t row, uint64_t col, const reim4_elem& value) { - REQUIRE_DRAMATICALLY(row < nrows, "rows out of bounds" << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "cols out of bounds" << col << " / " << ncols); - value.save_as(data + 8 * (row * ncols + col)); -}
@@ -1,95 +0,0 @@
-#ifndef SPQLIOS_REIM4_ELEM_H -#define SPQLIOS_REIM4_ELEM_H - -#include "test_commons.h" - -/** @brief test class representing one single reim4 element */ -class reim4_elem { - public: - /** @brief 8 components (4 real parts followed by 4 imag parts) */ - double value[8]; - /** @brief constructs from 4 real parts and 4 imaginary parts */ - reim4_elem(const double* re, const double* im); - /** @brief constructs from 8 components */ - explicit reim4_elem(const double* layout); - /** @brief zero */ - reim4_elem(); - /** @brief saves the real parts to re and the 4 imag to im */ - void save_re_im(double* re, double* im) const; - /** @brief saves the 8 components to reim4 */ - void save_as(double* reim4) const; - static reim4_elem zero(); -}; - -/** @brief checks for equality */ -bool operator==(const reim4_elem& x, const reim4_elem& y); -/** @brief random gaussian reim4 of stdev 1 and mean 0 */ -reim4_elem gaussian_reim4(); -/** @brief addition */ -reim4_elem operator+(const reim4_elem& x, const reim4_elem& y); -reim4_elem& operator+=(reim4_elem& x, const reim4_elem& y); -/** @brief subtraction */ -reim4_elem operator-(const reim4_elem& x, const reim4_elem& y); -reim4_elem& operator-=(reim4_elem& x, const reim4_elem& y); -/** @brief product */ -reim4_elem operator*(const reim4_elem& x, const reim4_elem& y); -std::ostream& operator<<(std::ostream& out, const reim4_elem& x); -/** @brief distance in infty norm */ -double infty_dist(const reim4_elem& x, const reim4_elem& y); - -/** @brief test class representing the view of one reim of m complexes */ -class reim4_array_view { - uint64_t size; ///< size of the reim array - double* data; ///< pointer to the start of the array - public: - /** @brief ininitializes a view at an existing given address */ - reim4_array_view(uint64_t size, double* data); - ; - /** @brief gets the i-th element */ - reim4_elem get(uint64_t i) const; - /** @brief sets the i-th element */ - void set(uint64_t i, const reim4_elem& value); -}; - -/** @brief test class representing the view of one matrix of nrowsxncols reim4's */ -class reim4_matrix_view { - uint64_t nrows; ///< number of rows - uint64_t ncols; ///< number of columns - double* data; ///< pointer to the start of the matrix - public: - /** @brief ininitializes a view at an existing given address */ - reim4_matrix_view(uint64_t nrows, uint64_t ncols, double* data); - /** @brief gets the i-th element */ - reim4_elem get(uint64_t row, uint64_t col) const; - /** @brief sets the i-th element */ - void set(uint64_t row, uint64_t col, const reim4_elem& value); -}; - -/** @brief test class representing the view of one reim of m complexes */ -class reim_view { - uint64_t m; ///< (complex) dimension of the reim polynomial - double* data; ///< address of the start of the reim polynomial - public: - /** @brief ininitializes a view at an existing given address */ - reim_view(uint64_t m, double* data); - ; - /** @brief extracts the i-th reim4 block (i<m/4) */ - reim4_elem get_blk(uint64_t i); - /** @brief sets the i-th reim4 block (i<m/4) */ - void set_blk(uint64_t i, const reim4_elem& value); -}; - -/** @brief view of one contiguous reim vector */ -class reim_vector_view { - uint64_t m; ///< (complex) dimension of the reim polynomial - uint64_t nrows; ///< number of reim polynomials - double* data; ///< address of the start of the reim polynomial - - public: - /** @brief ininitializes a view at an existing given address */ - reim_vector_view(uint64_t m, uint64_t nrows, double* data); - /** @brief view of the given reim */ - reim_view row(uint64_t row); -}; - -#endif // SPQLIOS_REIM4_ELEM_H
@@ -1,168 +0,0 @@
-// sha3.c -// 19-Nov-11 Markku-Juhani O. Saarinen <mjos@iki.fi> -// https://github.com/mjosaarinen/tiny_sha3 -// LICENSE: MIT - -// Revised 07-Aug-15 to match with official release of FIPS PUB 202 "SHA3" -// Revised 03-Sep-15 for portability + OpenSSL - style API - -#include "sha3.h" - -// update the state with given number of rounds - -void sha3_keccakf(uint64_t st[25]) { - // constants - const uint64_t keccakf_rndc[24] = {0x0000000000000001, 0x0000000000008082, 0x800000000000808a, 0x8000000080008000, - 0x000000000000808b, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009, - 0x000000000000008a, 0x0000000000000088, 0x0000000080008009, 0x000000008000000a, - 0x000000008000808b, 0x800000000000008b, 0x8000000000008089, 0x8000000000008003, - 0x8000000000008002, 0x8000000000000080, 0x000000000000800a, 0x800000008000000a, - 0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008}; - const int keccakf_rotc[24] = {1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, - 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44}; - const int keccakf_piln[24] = {10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1}; - - // variables - int i, j, r; - uint64_t t, bc[5]; - -#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__ - uint8_t* v; - - // endianess conversion. this is redundant on little-endian targets - for (i = 0; i < 25; i++) { - v = (uint8_t*)&st[i]; - st[i] = ((uint64_t)v[0]) | (((uint64_t)v[1]) << 8) | (((uint64_t)v[2]) << 16) | (((uint64_t)v[3]) << 24) | - (((uint64_t)v[4]) << 32) | (((uint64_t)v[5]) << 40) | (((uint64_t)v[6]) << 48) | (((uint64_t)v[7]) << 56); - } -#endif - - // actual iteration - for (r = 0; r < KECCAKF_ROUNDS; r++) { - // Theta - for (i = 0; i < 5; i++) bc[i] = st[i] ^ st[i + 5] ^ st[i + 10] ^ st[i + 15] ^ st[i + 20]; - - for (i = 0; i < 5; i++) { - t = bc[(i + 4) % 5] ^ ROTL64(bc[(i + 1) % 5], 1); - for (j = 0; j < 25; j += 5) st[j + i] ^= t; - } - - // Rho Pi - t = st[1]; - for (i = 0; i < 24; i++) { - j = keccakf_piln[i]; - bc[0] = st[j]; - st[j] = ROTL64(t, keccakf_rotc[i]); - t = bc[0]; - } - - // Chi - for (j = 0; j < 25; j += 5) { - for (i = 0; i < 5; i++) bc[i] = st[j + i]; - for (i = 0; i < 5; i++) st[j + i] ^= (~bc[(i + 1) % 5]) & bc[(i + 2) % 5]; - } - - // Iota - st[0] ^= keccakf_rndc[r]; - } - -#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__ - // endianess conversion. this is redundant on little-endian targets - for (i = 0; i < 25; i++) { - v = (uint8_t*)&st[i]; - t = st[i]; - v[0] = t & 0xFF; - v[1] = (t >> 8) & 0xFF; - v[2] = (t >> 16) & 0xFF; - v[3] = (t >> 24) & 0xFF; - v[4] = (t >> 32) & 0xFF; - v[5] = (t >> 40) & 0xFF; - v[6] = (t >> 48) & 0xFF; - v[7] = (t >> 56) & 0xFF; - } -#endif -} - -// Initialize the context for SHA3 - -int sha3_init(sha3_ctx_t* c, int mdlen) { - int i; - - for (i = 0; i < 25; i++) c->st.q[i] = 0; - c->mdlen = mdlen; - c->rsiz = 200 - 2 * mdlen; - c->pt = 0; - - return 1; -} - -// update state with more data - -int sha3_update(sha3_ctx_t* c, const void* data, size_t len) { - size_t i; - int j; - - j = c->pt; - for (i = 0; i < len; i++) { - c->st.b[j++] ^= ((const uint8_t*)data)[i]; - if (j >= c->rsiz) { - sha3_keccakf(c->st.q); - j = 0; - } - } - c->pt = j; - - return 1; -} - -// finalize and output a hash - -int sha3_final(void* md, sha3_ctx_t* c) { - int i; - - c->st.b[c->pt] ^= 0x06; - c->st.b[c->rsiz - 1] ^= 0x80; - sha3_keccakf(c->st.q); - - for (i = 0; i < c->mdlen; i++) { - ((uint8_t*)md)[i] = c->st.b[i]; - } - - return 1; -} - -// compute a SHA-3 hash (md) of given byte length from "in" - -void* sha3(const void* in, size_t inlen, void* md, int mdlen) { - sha3_ctx_t sha3; - - sha3_init(&sha3, mdlen); - sha3_update(&sha3, in, inlen); - sha3_final(md, &sha3); - - return md; -} - -// SHAKE128 and SHAKE256 extensible-output functionality - -void shake_xof(sha3_ctx_t* c) { - c->st.b[c->pt] ^= 0x1F; - c->st.b[c->rsiz - 1] ^= 0x80; - sha3_keccakf(c->st.q); - c->pt = 0; -} - -void shake_out(sha3_ctx_t* c, void* out, size_t len) { - size_t i; - int j; - - j = c->pt; - for (i = 0; i < len; i++) { - if (j >= c->rsiz) { - sha3_keccakf(c->st.q); - j = 0; - } - ((uint8_t*)out)[i] = c->st.b[j++]; - } - c->pt = j; -}
@@ -1,56 +0,0 @@
-// sha3.h -// 19-Nov-11 Markku-Juhani O. Saarinen <mjos@iki.fi> -// https://github.com/mjosaarinen/tiny_sha3 -// License: MIT - -#ifndef SHA3_H -#define SHA3_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include <stddef.h> -#include <stdint.h> - -#ifndef KECCAKF_ROUNDS -#define KECCAKF_ROUNDS 24 -#endif - -#ifndef ROTL64 -#define ROTL64(x, y) (((x) << (y)) | ((x) >> (64 - (y)))) -#endif - -// state context -typedef struct { - union { // state: - uint8_t b[200]; // 8-bit bytes - uint64_t q[25]; // 64-bit words - } st; - int pt, rsiz, mdlen; // these don't overflow -} sha3_ctx_t; - -// Compression function. -void sha3_keccakf(uint64_t st[25]); - -// OpenSSL - like interfece -int sha3_init(sha3_ctx_t* c, int mdlen); // mdlen = hash output in bytes -int sha3_update(sha3_ctx_t* c, const void* data, size_t len); -int sha3_final(void* md, sha3_ctx_t* c); // digest goes to md - -// compute a sha3 hash (md) of given byte length from "in" -void* sha3(const void* in, size_t inlen, void* md, int mdlen); - -// SHAKE128 and SHAKE256 extensible-output functions -#define shake128_init(c) sha3_init(c, 16) -#define shake256_init(c) sha3_init(c, 32) -#define shake_update sha3_update - -void shake_xof(sha3_ctx_t* c); -void shake_out(sha3_ctx_t* c, void* out, size_t len); - -#ifdef __cplusplus -} -#endif - -#endif // SHA3_H
@@ -1,10 +0,0 @@
-#include "test_commons.h" - -#include <inttypes.h> - -std::ostream& operator<<(std::ostream& out, __int128_t x) { - char c[35] = {0}; - snprintf(c, 35, "0x%016" PRIx64 "%016" PRIx64, uint64_t(x >> 64), uint64_t(x)); - return out << c; -} -std::ostream& operator<<(std::ostream& out, __uint128_t x) { return out << __int128_t(x); }
@@ -1,75 +0,0 @@
-#ifndef SPQLIOS_TEST_COMMONS_H -#define SPQLIOS_TEST_COMMONS_H - -#include <iostream> -#include <random> - -#include "../../spqlios/commons.h" - -/** @brief macro that crashes if the condition are not met */ -#define REQUIRE_DRAMATICALLY(req_contition, error_msg) \ - do { \ - if (!(req_contition)) { \ - std::cerr << "REQUIREMENT FAILED at " << __FILE__ << ":" << __LINE__ << ": " << error_msg << std::endl; \ - abort(); \ - } \ - } while (0) - -typedef std::default_random_engine test_rng; -/** @brief reference to the default test rng */ -test_rng& randgen(); -/** @brief uniformly random 64-bit uint */ -uint64_t uniform_u64(); -/** @brief uniformly random number <= 2^nbits-1 */ -uint64_t uniform_u64_bits(uint64_t nbits); -/** @brief uniformly random signed 64-bit number */ -int64_t uniform_i64(); -/** @brief uniformly random signed |number| <= 2^nbits */ -int64_t uniform_i64_bits(uint64_t nbits); -/** @brief uniformly random signed lb <= number <= ub */ -int64_t uniform_i64_bounds(const int64_t lb, const int64_t ub); -/** @brief uniformly random signed lb <= number <= ub */ -__int128_t uniform_i128_bounds(const __int128_t lb, const __int128_t ub); -/** @brief uniformly random gaussian float64 */ -double random_f64_gaussian(double stdev = 1); -/** @brief uniformly random signed lb <= number <= ub */ -double uniform_f64_bounds(const double lb, const double ub); -/** @brief uniformly random float64 in [0,1] */ -double uniform_f64_01(); -/** @brief random gaussian float64 */ -double random_f64_gaussian(double stdev); - -bool is_pow2(uint64_t n); - -void* alloc64(uint64_t size); - -typedef __uint128_t thash; -/** @brief returns some pseudorandom hash of a contiguous content */ -thash test_hash(const void* data, uint64_t size); -/** @brief class to return a pseudorandom hash of a piecewise-defined content */ -class test_hasher { - void* md; - - public: - test_hasher(); - test_hasher(const test_hasher&) = delete; - void operator=(const test_hasher&) = delete; - /** - * @brief append input bytes. - * The final hash only depends on the concatenation of bytes, not on the - * way the content was split into multiple calls to update. - */ - void update(const void* data, uint64_t size); - /** - * @brief returns the final hash. - * no more calls to update(...) shall be issued after this call. - */ - thash hash(); - ~test_hasher(); -}; - -// not included by default, since it makes some versions of gtest not compile -// std::ostream& operator<<(std::ostream& out, __int128_t x); -// std::ostream& operator<<(std::ostream& out, __uint128_t x); - -#endif // SPQLIOS_TEST_COMMONS_H
@@ -1,24 +0,0 @@
-#include "sha3.h" -#include "test_commons.h" - -/** @brief returns some pseudorandom hash of the content */ -thash test_hash(const void* data, uint64_t size) { - thash res; - sha3(data, size, &res, sizeof(res)); - return res; -} -/** @brief class to return a pseudorandom hash of the content */ -test_hasher::test_hasher() { - md = malloc(sizeof(sha3_ctx_t)); - sha3_init((sha3_ctx_t*)md, 16); -} - -void test_hasher::update(const void* data, uint64_t size) { sha3_update((sha3_ctx_t*)md, data, size); } - -thash test_hasher::hash() { - thash res; - sha3_final(&res, (sha3_ctx_t*)md); - return res; -} - -test_hasher::~test_hasher() { free(md); }
@@ -1,182 +0,0 @@
-#include "vec_rnx_layout.h" - -#include <cstring> - -#include "../../spqlios/arithmetic/vec_rnx_arithmetic.h" - -#ifdef VALGRIND_MEM_TESTS -#include "valgrind/memcheck.h" -#endif - -#define CANARY_PADDING (1024) -#define GARBAGE_VALUE (242) - -rnx_vec_f64_layout::rnx_vec_f64_layout(uint64_t n, uint64_t size, uint64_t slice) : n(n), size(size), slice(slice) { - REQUIRE_DRAMATICALLY(is_pow2(n), "not a power of 2" << n); - REQUIRE_DRAMATICALLY(slice >= n, "slice too small" << slice << " < " << n); - this->region = (uint8_t*)malloc(size * slice * sizeof(int64_t) + 2 * CANARY_PADDING); - this->data_start = (double*)(region + CANARY_PADDING); - // ensure that any invalid value is kind-of garbage - memset(region, GARBAGE_VALUE, size * slice * sizeof(int64_t) + 2 * CANARY_PADDING); - // mark inter-slice memory as not accessible -#ifdef VALGRIND_MEM_TESTS - VALGRIND_MAKE_MEM_NOACCESS(region, CANARY_PADDING); - VALGRIND_MAKE_MEM_NOACCESS(region + size * slice * sizeof(int64_t) + CANARY_PADDING, CANARY_PADDING); - for (uint64_t i = 0; i < size; ++i) { - VALGRIND_MAKE_MEM_UNDEFINED(data_start + i * slice, n * sizeof(int64_t)); - } - if (size != slice) { - for (uint64_t i = 0; i < size; ++i) { - VALGRIND_MAKE_MEM_NOACCESS(data_start + i * slice + n, (slice - n) * sizeof(int64_t)); - } - } -#endif -} - -rnx_vec_f64_layout::~rnx_vec_f64_layout() { free(region); } - -rnx_f64 rnx_vec_f64_layout::get_copy_zext(uint64_t index) const { - if (index < size) { - return rnx_f64(n, data_start + index * slice); - } else { - return rnx_f64::zero(n); - } -} - -rnx_f64 rnx_vec_f64_layout::get_copy(uint64_t index) const { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - return rnx_f64(n, data_start + index * slice); -} - -reim_fft64vec rnx_vec_f64_layout::get_dft_copy_zext(uint64_t index) const { - if (index < size) { - return reim_fft64vec(n, data_start + index * slice); - } else { - return reim_fft64vec::zero(n); - } -} - -reim_fft64vec rnx_vec_f64_layout::get_dft_copy(uint64_t index) const { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - return reim_fft64vec(n, data_start + index * slice); -} - -void rnx_vec_f64_layout::set(uint64_t index, const rnx_f64& elem) { - REQUIRE_DRAMATICALLY(index < size, "index overflow: " << index << " / " << size); - REQUIRE_DRAMATICALLY(elem.nn() == n, "incompatible ring dimensions: " << elem.nn() << " / " << n); - elem.save_as(data_start + index * slice); -} - -double* rnx_vec_f64_layout::data() { return data_start; } -const double* rnx_vec_f64_layout::data() const { return data_start; } - -void rnx_vec_f64_layout::fill_random(double log2bound) { - for (uint64_t i = 0; i < size; ++i) { - set(i, rnx_f64::random_log2bound(n, log2bound)); - } -} - -thash rnx_vec_f64_layout::content_hash() const { - test_hasher hasher; - for (uint64_t i = 0; i < size; ++i) { - hasher.update(data() + i * slice, n * sizeof(int64_t)); - } - return hasher.hash(); -} - -fft64_rnx_vmp_pmat_layout::fft64_rnx_vmp_pmat_layout(uint64_t n, uint64_t nrows, uint64_t ncols) - : nn(n), - nrows(nrows), - ncols(ncols), // - data((RNX_VMP_PMAT*)alloc64(nrows * ncols * nn * 8)) {} - -double* fft64_rnx_vmp_pmat_layout::get_addr(uint64_t row, uint64_t col, uint64_t blk) const { - REQUIRE_DRAMATICALLY(row < nrows, "row overflow: " << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "col overflow: " << col << " / " << ncols); - REQUIRE_DRAMATICALLY(blk < nn / 8, "block overflow: " << blk << " / " << (nn / 8)); - double* d = (double*)data; - if (col == (ncols - 1) && (ncols % 2 == 1)) { - // special case: last column out of an odd column number - return d + blk * nrows * ncols * 8 // major: blk - + col * nrows * 8 // col == ncols-1 - + row * 8; - } else { - // general case: columns go by pair - return d + blk * nrows * ncols * 8 // major: blk - + (col / 2) * (2 * nrows) * 8 // second: col pair index - + row * 2 * 8 // third: row index - + (col % 2) * 8; // minor: col in colpair - } -} - -reim4_elem fft64_rnx_vmp_pmat_layout::get(uint64_t row, uint64_t col, uint64_t blk) const { - return reim4_elem(get_addr(row, col, blk)); -} -reim4_elem fft64_rnx_vmp_pmat_layout::get_zext(uint64_t row, uint64_t col, uint64_t blk) const { - REQUIRE_DRAMATICALLY(blk < nn / 8, "block overflow: " << blk << " / " << (nn / 8)); - if (row < nrows && col < ncols) { - return reim4_elem(get_addr(row, col, blk)); - } else { - return reim4_elem::zero(); - } -} -void fft64_rnx_vmp_pmat_layout::set(uint64_t row, uint64_t col, uint64_t blk, const reim4_elem& value) const { - value.save_as(get_addr(row, col, blk)); -} - -fft64_rnx_vmp_pmat_layout::~fft64_rnx_vmp_pmat_layout() { spqlios_free(data); } - -reim_fft64vec fft64_rnx_vmp_pmat_layout::get_zext(uint64_t row, uint64_t col) const { - if (row >= nrows || col >= ncols) { - return reim_fft64vec::zero(nn); - } - if (nn < 8) { - // the pmat is just col major - double* addr = (double*)data + (row + col * nrows) * nn; - return reim_fft64vec(nn, addr); - } - // otherwise, reconstruct it block by block - reim_fft64vec res(nn); - for (uint64_t blk = 0; blk < nn / 8; ++blk) { - reim4_elem v = get(row, col, blk); - res.set_blk(blk, v); - } - return res; -} -void fft64_rnx_vmp_pmat_layout::set(uint64_t row, uint64_t col, const reim_fft64vec& value) { - REQUIRE_DRAMATICALLY(row < nrows, "row overflow: " << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "row overflow: " << col << " / " << ncols); - if (nn < 8) { - // the pmat is just col major - double* addr = (double*)data + (row + col * nrows) * nn; - value.save_as(addr); - return; - } - // otherwise, reconstruct it block by block - for (uint64_t blk = 0; blk < nn / 8; ++blk) { - reim4_elem v = value.get_blk(blk); - set(row, col, blk, v); - } -} -void fft64_rnx_vmp_pmat_layout::fill_random(double log2bound) { - for (uint64_t row = 0; row < nrows; ++row) { - for (uint64_t col = 0; col < ncols; ++col) { - set(row, col, reim_fft64vec::random(nn, log2bound)); - } - } -} - -fft64_rnx_svp_ppol_layout::fft64_rnx_svp_ppol_layout(uint64_t n) - : nn(n), // - data((RNX_SVP_PPOL*)alloc64(nn * 8)) {} - -reim_fft64vec fft64_rnx_svp_ppol_layout::get_copy() const { return reim_fft64vec(nn, (double*)data); } - -void fft64_rnx_svp_ppol_layout::set(const reim_fft64vec& value) { value.save_as((double*)data); } - -void fft64_rnx_svp_ppol_layout::fill_dft_random(uint64_t log2bound) { set(reim_fft64vec::dft_random(nn, log2bound)); } - -void fft64_rnx_svp_ppol_layout::fill_random(double log2bound) { set(reim_fft64vec::random(nn, log2bound)); } - -fft64_rnx_svp_ppol_layout::~fft64_rnx_svp_ppol_layout() { spqlios_free(data); } -thash fft64_rnx_svp_ppol_layout::content_hash() const { return test_hash(data, nn * sizeof(double)); }
@@ -1,85 +0,0 @@
-#ifndef SPQLIOS_EXT_VEC_RNX_LAYOUT_H -#define SPQLIOS_EXT_VEC_RNX_LAYOUT_H - -#include "../../spqlios/arithmetic/vec_rnx_arithmetic.h" -#include "fft64_dft.h" -#include "negacyclic_polynomial.h" -#include "reim4_elem.h" -#include "test_commons.h" - -/** @brief a test memory layout for rnx i64 polynomials vectors */ -class rnx_vec_f64_layout { - uint64_t n; - uint64_t size; - uint64_t slice; - double* data_start; - uint8_t* region; - - public: - // NO-COPY structure - rnx_vec_f64_layout(const rnx_vec_f64_layout&) = delete; - void operator=(const rnx_vec_f64_layout&) = delete; - rnx_vec_f64_layout(rnx_vec_f64_layout&&) = delete; - void operator=(rnx_vec_f64_layout&&) = delete; - /** @brief initialises a memory layout */ - rnx_vec_f64_layout(uint64_t n, uint64_t size, uint64_t slice); - /** @brief destructor */ - ~rnx_vec_f64_layout(); - - /** @brief get a copy of item index index (extended with zeros) */ - rnx_f64 get_copy_zext(uint64_t index) const; - /** @brief get a copy of item index index (extended with zeros) */ - rnx_f64 get_copy(uint64_t index) const; - /** @brief get a copy of item index index (extended with zeros) */ - reim_fft64vec get_dft_copy_zext(uint64_t index) const; - /** @brief get a copy of item index index (extended with zeros) */ - reim_fft64vec get_dft_copy(uint64_t index) const; - - /** @brief get a copy of item index index (index<size) */ - void set(uint64_t index, const rnx_f64& elem); - /** @brief fill with random values */ - void fill_random(double log2bound = 0); - /** @brief raw pointer access */ - double* data(); - /** @brief raw pointer access (const version) */ - const double* data() const; - /** @brief content hashcode */ - thash content_hash() const; -}; - -/** @brief test layout for the VMP_PMAT */ -class fft64_rnx_vmp_pmat_layout { - public: - const uint64_t nn; - const uint64_t nrows; - const uint64_t ncols; - RNX_VMP_PMAT* const data; - fft64_rnx_vmp_pmat_layout(uint64_t n, uint64_t nrows, uint64_t ncols); - double* get_addr(uint64_t row, uint64_t col, uint64_t blk) const; - reim4_elem get(uint64_t row, uint64_t col, uint64_t blk) const; - thash content_hash() const; - reim4_elem get_zext(uint64_t row, uint64_t col, uint64_t blk) const; - reim_fft64vec get_zext(uint64_t row, uint64_t col) const; - void set(uint64_t row, uint64_t col, uint64_t blk, const reim4_elem& v) const; - void set(uint64_t row, uint64_t col, const reim_fft64vec& value); - /** @brief fill with random double values (unstructured) */ - void fill_random(double log2bound); - ~fft64_rnx_vmp_pmat_layout(); -}; - -/** @brief test layout for the SVP_PPOL */ -class fft64_rnx_svp_ppol_layout { - public: - const uint64_t nn; - RNX_SVP_PPOL* const data; - fft64_rnx_svp_ppol_layout(uint64_t n); - thash content_hash() const; - reim_fft64vec get_copy() const; - void set(const reim_fft64vec&); - /** @brief fill with random double values (unstructured) */ - void fill_random(double log2bound); - /** @brief fill with random ffts of small int polynomials */ - void fill_dft_random(uint64_t log2bound); - ~fft64_rnx_svp_ppol_layout(); -}; -#endif // SPQLIOS_EXT_VEC_RNX_LAYOUT_H
@@ -1,55 +0,0 @@
-#include "zn_layouts.h" - -zn32_pmat_layout::zn32_pmat_layout(uint64_t nrows, uint64_t ncols) - : nrows(nrows), // - ncols(ncols), // - data((ZN32_VMP_PMAT*)malloc((nrows * ncols + 7) * sizeof(int32_t))) {} - -zn32_pmat_layout::~zn32_pmat_layout() { free(data); } - -int32_t* zn32_pmat_layout::get_addr(uint64_t row, uint64_t col) const { - REQUIRE_DRAMATICALLY(row < nrows, "row overflow" << row << " / " << nrows); - REQUIRE_DRAMATICALLY(col < ncols, "col overflow" << col << " / " << ncols); - const uint64_t nblk = ncols >> 5; - const uint64_t rem_ncols = ncols & 31; - uint64_t blk = col >> 5; - uint64_t col_rem = col & 31; - if (blk < nblk) { - // column is part of a full block - return (int32_t*)data + blk * nrows * 32 + row * 32 + col_rem; - } else { - // column is part of the last block - return (int32_t*)data + blk * nrows * 32 + row * rem_ncols + col_rem; - } -} -int32_t zn32_pmat_layout::get(uint64_t row, uint64_t col) const { return *get_addr(row, col); } -int32_t zn32_pmat_layout::get_zext(uint64_t row, uint64_t col) const { - if (row >= nrows || col >= ncols) return 0; - return *get_addr(row, col); -} -void zn32_pmat_layout::set(uint64_t row, uint64_t col, int32_t value) { *get_addr(row, col) = value; } -void zn32_pmat_layout::fill_random() { - int32_t* d = (int32_t*)data; - for (uint64_t i = 0; i < nrows * ncols; ++i) d[i] = uniform_i64_bits(32); -} -thash zn32_pmat_layout::content_hash() const { return test_hash(data, nrows * ncols * sizeof(int32_t)); } - -template <typename T> -std::vector<int32_t> vmp_product(const T* vec, uint64_t vec_size, uint64_t out_size, const zn32_pmat_layout& mat) { - uint64_t rows = std::min(vec_size, mat.nrows); - uint64_t cols = std::min(out_size, mat.ncols); - std::vector<int32_t> res(out_size, 0); - for (uint64_t j = 0; j < cols; ++j) { - for (uint64_t i = 0; i < rows; ++i) { - res[j] += vec[i] * mat.get(i, j); - } - } - return res; -} - -template std::vector<int32_t> vmp_product(const int8_t* vec, uint64_t vec_size, uint64_t out_size, - const zn32_pmat_layout& mat); -template std::vector<int32_t> vmp_product(const int16_t* vec, uint64_t vec_size, uint64_t out_size, - const zn32_pmat_layout& mat); -template std::vector<int32_t> vmp_product(const int32_t* vec, uint64_t vec_size, uint64_t out_size, - const zn32_pmat_layout& mat);
@@ -1,29 +0,0 @@
-#ifndef SPQLIOS_EXT_ZN_LAYOUTS_H -#define SPQLIOS_EXT_ZN_LAYOUTS_H - -#include "../../spqlios/arithmetic/zn_arithmetic.h" -#include "test_commons.h" - -class zn32_pmat_layout { - public: - const uint64_t nrows; - const uint64_t ncols; - ZN32_VMP_PMAT* const data; - zn32_pmat_layout(uint64_t nrows, uint64_t ncols); - - private: - int32_t* get_addr(uint64_t row, uint64_t col) const; - - public: - int32_t get(uint64_t row, uint64_t col) const; - int32_t get_zext(uint64_t row, uint64_t col) const; - void set(uint64_t row, uint64_t col, int32_t value); - void fill_random(); - thash content_hash() const; - ~zn32_pmat_layout(); -}; - -template <typename T> -std::vector<int32_t> vmp_product(const T* vec, uint64_t vec_size, uint64_t out_size, const zn32_pmat_layout& mat); - -#endif // SPQLIOS_EXT_ZN_LAYOUTS_H
@@ -11,8 +11,8 @@
[dependencies] rug = {workspace = true} criterion = {workspace = true} -poulpy-hal = "0.1.2" -poulpy-backend = "0.1.2" +poulpy-hal = {path="../poulpy-hal"} +poulpy-backend = {path="../poulpy-backend"} itertools = {workspace = true} byteorder = {workspace = true}
@@ -0,0 +1,172 @@
+# 🐙 Poulpy-Core + +**Poulpy-Core** is a Rust crate built on **`poulpy-hal`**, providing scheme- and backend-agnostic RLWE-based homomorphic encryption building blocks. + +## Getting Started + +```rust +use poulpy_backend::cpu_spqlios::FFT64; +use poulpy_core::{ + GLWEOperations, SIGMA, + layouts::{ + GLWECiphertext, GLWEPlaintext, GLWESecret, Infos, + prepared::{GLWESecretPrepared, PrepareAlloc}, + }, +}; +use poulpy_hal::{ + api::{ModuleNew, ScratchOwnedAlloc, ScratchOwnedBorrow, VecZnxFillUniform}, + layouts::{Module, ScratchOwned}, + source::Source, +}; + +fn main() { + // Ring degree + let log_n: usize = 10; + + let n: usize = 1<<log_n; + + // Base-2-k (implicit digit decomposition) + let basek: usize = 14; + + // Ciphertext Torus precision (equivalent to ciphertext modulus) + let k_ct: usize = 27; + + // Plaintext Torus precision (equivament to plaintext modulus) + let k_pt: usize = basek; + + // GLWE rank + let rank: usize = 1; + + // Instantiate Module (DFT Tables) + let module: Module<FFT64> = Module::<FFT64>::new(n as u64); + + // Allocates ciphertext & plaintexts + let mut ct: GLWECiphertext<Vec<u8>> = GLWECiphertext::alloc(n, basek, k_ct, rank); + let mut pt_want: GLWEPlaintext<Vec<u8>> = GLWEPlaintext::alloc(n, basek, k_pt); + let mut pt_have: GLWEPlaintext<Vec<u8>> = GLWEPlaintext::alloc(n, basek, k_pt); + + // CPRNG + let mut source_xs: Source = Source::new([0u8; 32]); + let mut source_xe: Source = Source::new([1u8; 32]); + let mut source_xa: Source = Source::new([2u8; 32]); + + // Scratch space + let mut scratch: ScratchOwned<FFT64> = ScratchOwned::alloc( + GLWECiphertext::encrypt_sk_scratch_space(&module, n, basek, ct.k()) + | GLWECiphertext::decrypt_scratch_space(&module, n, basek, ct.k()), + ); + + // Generate secret-key + let mut sk: GLWESecret<Vec<u8>> = GLWESecret::alloc(n, rank); + sk.fill_ternary_prob(0.5, &mut source_xs); + + // Backend-prepared secret + let sk_prepared: GLWESecretPrepared<Vec<u8>, FFT64> = sk.prepare_alloc(&module, scratch.borrow()); + + // Uniform plaintext + module.vec_znx_fill_uniform(basek, &mut pt_want.data, 0, k_pt, &mut source_xa); + + // Encryption + ct.encrypt_sk( + &module, + &pt_want, + &sk_prepared, + &mut source_xa, + &mut source_xe, + scratch.borrow(), + ); + + // Decryption + ct.decrypt(&module, &mut pt_have, &sk_prepared, scratch.borrow()); + + // Diff between pt - Dec(Enc(pt)) + pt_want.sub_inplace_ab(&module, &pt_have); + + // Ideal vs. actual noise + let noise_have: f64 = pt_want.data.std(basek, 0) * (ct.k() as f64).exp2(); + let noise_want: f64 = SIGMA; + + // Check + assert!(noise_have <= noise_want + 0.2); +} +``` + +## Layouts + +This crate defines three categories of layouts for `LWE`, `GLWE`, `GGLWE`, and `GGSW` objects (and their derivatives), all instantiated using **`poulpy-hal`** layouts. Each serves a distinct purpose: + +* **Standard** → Front-end, serializable layouts. These are backend-agnostic and act as inputs/outputs of computations (e.g., `GGLWEAutomorphismKey`). +* **Compressed** → Compact serializable variants of the standard layouts. They are not usable for computation but significantly reduce storage size (e.g., `GGLWEAutomorphismKeyCompressed`). +* **Prepared** → Backend-optimized, opaque layouts used only for computation (write-only). These store preprocessed data for efficient execution on a specific backend (e.g., `GGLWEAutomorphismKeyPrepared`). + +All **standard** and **compressed** layouts implement the `WriterTo` and `ReaderFrom` traits, enabling straightforward serialization/deserialization with any type implementing `Write` or `Read`: + +```rust +pub trait WriterTo { + fn write_to<W: Write>(&self, writer: &mut W) -> Result<()>; +} + +pub trait ReaderFrom { + fn read_from<R: Read>(&mut self, reader: &mut R) -> Result<()>; +} +``` + +### Example Workflow + +```mermaid +flowchart TD + A[GGLWEAutomorphismKeyCompressed]-->|decompress|B[GGLWEAutomorphismKey]-->|prepare|C[GGLWEAutomorphismKeyPrepared] +``` + +Equivalent Rust: + +```rust +let mut atk_compressed: GGLWEAutomorphismKeyCompressed<Vec<u8>> = + GGLWEAutomorphismKeyCompressed::alloc(...); +let mut atk: GGLWEAutomorphismKey<Vec<u8>> = + GGLWEAutomorphismKey::alloc(...); +atk.decompress(module, &atk_compressed); +let mut atk_prep: GGLWEAutomorphismKeyPrepared<Vec<u8>, B> = GLWESecretPrepared<Vec<u8>, B> = atk.prepare_alloc(...); +``` + +--- + +## Encryption & Decryption + +* **Encryption** → Supported for all **standard** and **compressed** layouts. +* **Decryption** → Only directly available for `LWECiphertext` and `GLWECiphertext`. + However, it remains naturally usable on `GGLWE` and `GGSW` objects, since these are vectors/matrices of `GLWECiphertext`. + +```rust +let mut atk: GGLWEAutomorphismKey<Vec<u8>> = + GGLWEAutomorphismKey::alloc(...); +atk.encrypt_sk(...); +atk.at(row, 0).decrypt(...); +``` +## Keyswitching, Automorphism & External Product + +Keyswitching, automorphisms, and external products are supported for all ciphertext types where they are well-defined. +This includes subtypes such as `GGLWEAutomorphismKey`. + +For example: + +```rust +atk.external_product(...); +ggsw.automorphism(...); +``` + +--- + +## Additional Features + +* Ciphertexts: `LWE` and `GLWE` +* `GLWE` ring packing +* `GLWE` trace +* Noise analysis for `GLWE`, `GGLWE`, `GGSW` +* Basic operations over `GLWE` ciphertexts and plaintexts + +--- + +## Tests + +A fully generic test suite is available in [`src/tests/generics`](./src/tests/generics).
@@ -37,7 +37,6 @@ let rank: usize = p.rank;
let digits: usize = 1; let rows: usize = 1; //(p.k_ct_in.div_ceil(p.basek); - let sigma: f64 = 3.2; let mut ct_ggsw: GGSWCiphertext<Vec<u8>> = GGSWCiphertext::alloc(n, basek, k_ggsw, rows, digits, rank); let mut ct_glwe_in: GLWECiphertext<Vec<u8>> = GLWECiphertext::alloc(n, basek, k_ct_in, rank);@@ -73,7 +72,6 @@ &pt_rgsw,
&sk_dft, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -82,7 +80,6 @@ &module,
&sk_dft, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -134,7 +131,6 @@ let rank: usize = p.rank;
let digits: usize = 1; let rows: usize = p.k_ct.div_ceil(p.basek); - let sigma: f64 = 3.2; let mut ct_ggsw: GGSWCiphertext<Vec<u8>> = GGSWCiphertext::alloc(n, basek, k_ggsw, rows, digits, rank); let mut ct_glwe: GLWECiphertext<Vec<u8>> = GLWECiphertext::alloc(n, basek, k_glwe, rank);@@ -168,7 +164,6 @@ &pt_rgsw,
&sk_dft, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -177,7 +172,6 @@ &module,
&sk_dft, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );
@@ -39,7 +39,6 @@ let rank_out: usize = p.rank_out;
let digits: usize = p.digits; let rows: usize = p.k_ct_in.div_ceil(p.basek * digits); - let sigma: f64 = 3.2; let mut ksk: GGLWEAutomorphismKey<Vec<u8>> = GGLWEAutomorphismKey::alloc(n, basek, k_grlwe, rows, digits, rank_out); let mut ct_in: GLWECiphertext<Vec<u8>> = GLWECiphertext::alloc(n, basek, k_rlwe_in, rank_in);@@ -78,7 +77,6 @@ -1,
&sk_in, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -87,7 +85,6 @@ &module,
&sk_in_dft, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -146,7 +143,6 @@ let rank: usize = p.rank;
let digits: usize = 1; let rows: usize = p.k_ct.div_ceil(p.basek); - let sigma: f64 = 3.2; let mut ksk: GGLWESwitchingKey<Vec<u8>> = GGLWESwitchingKey::alloc(n, basek, k_ksk, rows, digits, rank, rank); let mut ct: GLWECiphertext<Vec<u8>> = GLWECiphertext::alloc(n, basek, k_ct, rank);@@ -174,7 +170,6 @@ &sk_in,
&sk_out, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -183,7 +178,6 @@ &module,
&sk_in_dft, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );
@@ -0,0 +1,84 @@
+use poulpy_backend::cpu_spqlios::FFT64; +use poulpy_core::{ + GLWEOperations, SIGMA, + layouts::{ + GLWECiphertext, GLWEPlaintext, GLWESecret, Infos, + prepared::{GLWESecretPrepared, PrepareAlloc}, + }, +}; +use poulpy_hal::{ + api::{ModuleNew, ScratchOwnedAlloc, ScratchOwnedBorrow, VecZnxFillUniform}, + layouts::{Module, ScratchOwned}, + source::Source, +}; + +fn main() { + // Ring degree + let log_n: usize = 10; + + let n: usize = 1 << log_n; + + // Base-2-k (implicit digit decomposition) + let basek: usize = 14; + + // Ciphertext Torus precision (equivalent to ciphertext modulus) + let k_ct: usize = 27; + + // Plaintext Torus precision (equivament to plaintext modulus) + let k_pt: usize = basek; + + // GLWE rank + let rank: usize = 1; + + // Instantiate Module (DFT Tables) + let module: Module<FFT64> = Module::<FFT64>::new(n as u64); + + // Allocates ciphertext & plaintexts + let mut ct: GLWECiphertext<Vec<u8>> = GLWECiphertext::alloc(n, basek, k_ct, rank); + let mut pt_want: GLWEPlaintext<Vec<u8>> = GLWEPlaintext::alloc(n, basek, k_pt); + let mut pt_have: GLWEPlaintext<Vec<u8>> = GLWEPlaintext::alloc(n, basek, k_pt); + + // CPRNG + let mut source_xs: Source = Source::new([0u8; 32]); + let mut source_xe: Source = Source::new([1u8; 32]); + let mut source_xa: Source = Source::new([2u8; 32]); + + // Scratch space + let mut scratch: ScratchOwned<FFT64> = ScratchOwned::alloc( + GLWECiphertext::encrypt_sk_scratch_space(&module, n, basek, ct.k()) + | GLWECiphertext::decrypt_scratch_space(&module, n, basek, ct.k()), + ); + + // Generate secret-key + let mut sk: GLWESecret<Vec<u8>> = GLWESecret::alloc(n, rank); + sk.fill_ternary_prob(0.5, &mut source_xs); + + // Backend-prepared secret + let sk_prepared: GLWESecretPrepared<Vec<u8>, FFT64> = sk.prepare_alloc(&module, scratch.borrow()); + + // Uniform plaintext + module.vec_znx_fill_uniform(basek, &mut pt_want.data, 0, k_pt, &mut source_xa); + + // Encryption + ct.encrypt_sk( + &module, + &pt_want, + &sk_prepared, + &mut source_xa, + &mut source_xe, + scratch.borrow(), + ); + + // Decryption + ct.decrypt(&module, &mut pt_have, &sk_prepared, scratch.borrow()); + + // Diff between pt - Dec(Enc(pt)) + pt_want.sub_inplace_ab(&module, &pt_have); + + // Ideal vs. actual noise + let noise_have: f64 = pt_want.data.std(basek, 0) * (ct.k() as f64).exp2(); + let noise_want: f64 = SIGMA; + + // Check + assert!(noise_have <= noise_want + 0.2); +}
@@ -35,7 +35,6 @@ p: i64,
sk: &GLWESecret<DataSk>, seed_xa: [u8; 32], source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: VecZnxAutomorphism@@ -97,7 +96,7 @@ });
} self.key - .encrypt_sk(module, sk, &sk_out, seed_xa, source_xe, sigma, scratch_1); + .encrypt_sk(module, sk, &sk_out, seed_xa, source_xe, scratch_1); self.p = p; }
@@ -10,7 +10,7 @@ };
use crate::{ TakeGLWEPt, - encryption::glwe_encrypt_sk_internal, + encryption::{SIGMA, glwe_encrypt_sk_internal}, layouts::{GGLWECiphertext, Infos, compressed::GGLWECiphertextCompressed, prepared::GLWESecretPrepared}, };@@ -32,7 +32,6 @@ pt: &ScalarZnx<DataPt>,
sk: &GLWESecretPrepared<DataSk, B>, seed: [u8; 32], source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: VecZnxAddScalarInplace@@ -128,7 +127,7 @@ Some((&tmp_pt, 0)),
sk, &mut source_xa_tmp, source_xe, - sigma, + SIGMA, scrach_1, ); });
@@ -41,7 +41,6 @@ sk_in: &GLWESecret<DataSkIn>,
sk_out: &GLWESecret<DataSkOut>, seed_xa: [u8; 32], source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: SvpPrepare<B>@@ -119,7 +118,6 @@ &sk_in_tmp,
&sk_out_tmp, seed_xa, source_xe, - sigma, scratch2, ); self.sk_in_n = sk_in.n();
@@ -32,7 +32,6 @@ module: &Module<B>,
sk: &GLWESecret<DataSk>, seed_xa: [u8; 32], source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: SvpApply<B>@@ -104,7 +103,7 @@
let (seed_xa_tmp, _) = source_xa.branch(); self.at_mut(i, j) - .encrypt_sk(module, &sk_ij, sk, seed_xa_tmp, source_xe, sigma, scratch5); + .encrypt_sk(module, &sk_ij, sk, seed_xa_tmp, source_xe, scratch5); }); }) }
@@ -10,7 +10,7 @@ };
use crate::{ TakeGLWEPt, - encryption::glwe_encrypt_sk_internal, + encryption::{SIGMA, glwe_encrypt_sk_internal}, layouts::{GGSWCiphertext, Infos, compressed::GGSWCiphertextCompressed, prepared::GLWESecretPrepared}, };@@ -32,7 +32,6 @@ pt: &ScalarZnx<DataPt>,
sk: &GLWESecretPrepared<DataSk, B>, seed_xa: [u8; 32], source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: VecZnxAddScalarInplace@@ -97,7 +96,7 @@ Some((&tmp_pt, col_j)),
sk, &mut source_xa_tmp, source_xe, - sigma, + SIGMA, scratch_1, ); });
@@ -9,7 +9,7 @@ source::Source,
}; use crate::{ - encryption::glwe_ct::glwe_encrypt_sk_internal, + encryption::{SIGMA, glwe_ct::glwe_encrypt_sk_internal}, layouts::{GLWECiphertext, GLWEPlaintext, Infos, compressed::GLWECiphertextCompressed, prepared::GLWESecretPrepared}, };@@ -31,7 +31,6 @@ pt: &GLWEPlaintext<DataPt>,
sk: &GLWESecretPrepared<DataSk, B>, seed_xa: [u8; 32], source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: VecZnxDftAllocBytes@@ -49,15 +48,7 @@ + VecZnxNormalize<B>
+ VecZnxSub, Scratch<B>: TakeVecZnxDft<B> + ScratchAvailable + TakeVecZnx, { - self.encrypt_sk_internal( - module, - Some((pt, 0)), - sk, - seed_xa, - source_xe, - sigma, - scratch, - ); + self.encrypt_sk_internal(module, Some((pt, 0)), sk, seed_xa, source_xe, scratch); } #[allow(clippy::too_many_arguments)]@@ -68,7 +59,6 @@ pt: Option<(&GLWEPlaintext<DataPt>, usize)>,
sk: &GLWESecretPrepared<DataSk, B>, seed_xa: [u8; 32], source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: VecZnxDftAllocBytes@@ -99,7 +89,7 @@ pt,
sk, &mut source_xa, source_xe, - sigma, + SIGMA, scratch, ); self.seed = seed_xa;
@@ -36,7 +36,6 @@ p: i64,
sk: &GLWESecret<DataSk>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: VecZnxAddScalarInplace@@ -92,7 +91,7 @@ });
} self.key - .encrypt_sk(module, sk, &sk_out, source_xa, source_xe, sigma, scratch_1); + .encrypt_sk(module, sk, &sk_out, source_xa, source_xe, scratch_1); self.p = p; }
@@ -36,7 +36,6 @@ pt: &ScalarZnx<DataPt>,
sk: &GLWESecretPrepared<DataSk, B>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: VecZnxAddScalarInplace@@ -127,7 +126,7 @@ module.vec_znx_normalize_inplace(basek, &mut tmp_pt.data, 0, scrach_1);
// rlwe encrypt of vec_znx_pt into vec_znx_ct self.at_mut(row_i, col_i) - .encrypt_sk(module, &tmp_pt, sk, source_xa, source_xe, sigma, scrach_1); + .encrypt_sk(module, &tmp_pt, sk, source_xa, source_xe, scrach_1); }); }); }
@@ -52,7 +52,6 @@ sk_in: &GLWESecret<DataSkIn>,
sk_out: &GLWESecret<DataSkOut>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: VecZnxAddScalarInplace@@ -130,7 +129,6 @@ &sk_in_tmp,
&sk_out_tmp, source_xa, source_xe, - sigma, scratch2, ); self.sk_in_n = sk_in.n();
@@ -39,7 +39,6 @@ module: &Module<B>,
sk: &GLWESecret<DataSk>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: SvpApply<B>@@ -102,7 +101,7 @@ scratch5,
); self.at_mut(i, j) - .encrypt_sk(module, &sk_ij, sk, source_xa, source_xe, sigma, scratch5); + .encrypt_sk(module, &sk_ij, sk, source_xa, source_xe, scratch5); }); }) }
@@ -35,7 +35,6 @@ pt: &ScalarZnx<DataPt>,
sk: &GLWESecretPrepared<DataSk, B>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: VecZnxAddScalarInplace@@ -86,7 +85,6 @@ Some((&tmp_pt, col_j)),
sk, source_xa, source_xe, - sigma, scratch1, ); });
@@ -10,8 +10,8 @@ source::Source,
}; use crate::{ - SIX_SIGMA, dist::Distribution, + encryption::{SIGMA, SIGMA_BOUND}, layouts::{ GLWECiphertext, GLWEPlaintext, Infos, prepared::{GLWEPublicKeyPrepared, GLWESecretPrepared},@@ -46,7 +46,6 @@ pt: &GLWEPlaintext<DataPt>,
sk: &GLWESecretPrepared<DataSk, B>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: VecZnxDftAllocBytes@@ -77,15 +76,7 @@ GLWECiphertext::encrypt_sk_scratch_space(module, self.n(), self.basek(), self.k())
) } - self.encrypt_sk_internal( - module, - Some((pt, 0)), - sk, - source_xa, - source_xe, - sigma, - scratch, - ); + self.encrypt_sk_internal(module, Some((pt, 0)), sk, source_xa, source_xe, scratch); } pub fn encrypt_zero_sk<DataSk: DataRef, B: Backend>(@@ -94,7 +85,6 @@ module: &Module<B>,
sk: &GLWESecretPrepared<DataSk, B>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: VecZnxDftAllocBytes@@ -129,7 +119,6 @@ None::<(&GLWEPlaintext<Vec<u8>>, usize)>,
sk, source_xa, source_xe, - sigma, scratch, ); }@@ -142,7 +131,6 @@ pt: Option<(&GLWEPlaintext<DataPt>, usize)>,
sk: &GLWESecretPrepared<DataSk, B>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: VecZnxDftAllocBytes@@ -172,7 +160,7 @@ pt,
sk, source_xa, source_xe, - sigma, + SIGMA, scratch, ); }@@ -185,7 +173,6 @@ pt: &GLWEPlaintext<DataPt>,
pk: &GLWEPublicKeyPrepared<DataPk, B>, source_xu: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: SvpPrepare<B>@@ -196,15 +183,7 @@ + VecZnxBigAddSmallInplace<B>
+ VecZnxBigNormalize<B>, Scratch<B>: TakeSvpPPol<B> + TakeScalarZnx + TakeVecZnxDft<B>, { - self.encrypt_pk_internal::<DataPt, DataPk, B>( - module, - Some((pt, 0)), - pk, - source_xu, - source_xe, - sigma, - scratch, - ); + self.encrypt_pk_internal::<DataPt, DataPk, B>(module, Some((pt, 0)), pk, source_xu, source_xe, scratch); } pub fn encrypt_zero_pk<DataPk: DataRef, B: Backend>(@@ -213,7 +192,6 @@ module: &Module<B>,
pk: &GLWEPublicKeyPrepared<DataPk, B>, source_xu: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: SvpPrepare<B>@@ -230,7 +208,6 @@ None::<(&GLWEPlaintext<Vec<u8>>, usize)>,
pk, source_xu, source_xe, - sigma, scratch, ); }@@ -243,7 +220,6 @@ pt: Option<(&GLWEPlaintext<DataPt>, usize)>,
pk: &GLWEPublicKeyPrepared<DataPk, B>, source_xu: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where Module<B>: SvpPrepare<B>@@ -300,15 +276,7 @@ // ci_big = u * p[i]
let mut ci_big = module.vec_znx_dft_to_vec_znx_big_consume(ci_dft); // ci_big = u * pk[i] + e - module.vec_znx_big_add_normal( - basek, - &mut ci_big, - 0, - pk.k(), - source_xe, - sigma, - sigma * SIX_SIGMA, - ); + module.vec_znx_big_add_normal(basek, &mut ci_big, 0, pk.k(), source_xe, SIGMA, SIGMA_BOUND); // ci_big = u * pk[i] + e + m (if col = i) if let Some((pt, col)) = pt@@ -412,7 +380,7 @@ });
} // c[0] += e - module.vec_znx_add_normal(basek, &mut c0, 0, k, source_xe, sigma, sigma * SIX_SIGMA); + module.vec_znx_add_normal(basek, &mut c0, 0, k, source_xe, sigma, SIGMA_BOUND); // c[0] += m if col = 0 if let Some((pt, col)) = pt
@@ -18,7 +18,6 @@ module: &Module<B>,
sk: &GLWESecretPrepared<S, B>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, ) where Module<B>:, Module<B>: VecZnxDftAllocBytes@@ -61,7 +60,7 @@ self.k(),
)); let mut tmp: GLWECiphertext<Vec<u8>> = GLWECiphertext::alloc(self.n(), self.basek(), self.k(), self.rank()); - tmp.encrypt_zero_sk(module, sk, source_xa, source_xe, sigma, scratch.borrow()); + tmp.encrypt_zero_sk(module, sk, source_xa, source_xe, scratch.borrow()); self.dist = sk.dist; } }
@@ -34,7 +34,6 @@ sk_lwe: &LWESecret<DLwe>,
sk_glwe: &GLWESecret<DGlwe>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where DLwe: DataRef,@@ -75,7 +74,6 @@ sk_glwe,
&sk_lwe_as_glwe, source_xa, source_xe, - sigma, scratch1, ); }
@@ -8,7 +8,7 @@ source::Source,
}; use crate::{ - SIX_SIGMA, + encryption::{SIGMA, SIGMA_BOUND}, layouts::{Infos, LWECiphertext, LWEPlaintext, LWESecret}, };@@ -20,7 +20,6 @@ pt: &LWEPlaintext<DataPt>,
sk: &LWESecret<DataSk>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, ) where DataPt: DataRef, DataSk: DataRef,@@ -58,15 +57,7 @@ .map(|(x, y)| x * y)
.sum::<i64>(); }); - module.vec_znx_add_normal( - basek, - &mut self.data, - 0, - k, - source_xe, - sigma, - sigma * SIX_SIGMA, - ); + module.vec_znx_add_normal(basek, &mut self.data, 0, k, source_xe, SIGMA, SIGMA_BOUND); module.vec_znx_normalize_inplace( basek,
@@ -34,7 +34,6 @@ sk_lwe_in: &LWESecret<DIn>,
sk_lwe_out: &LWESecret<DOut>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where DIn: DataRef,@@ -83,7 +82,6 @@ &sk_in_glwe,
&sk_out_glwe, source_xa, source_xe, - sigma, scratch2, ); }
@@ -32,7 +32,6 @@ sk_lwe: &LWESecret<DLwe>,
sk_glwe: &GLWESecret<DGlwe>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where DLwe: DataRef,@@ -73,7 +72,6 @@ &sk_lwe_as_glwe,
sk_glwe, source_xa, source_xe, - sigma, scratch1, ); }
@@ -12,3 +12,6 @@ mod lwe_ksk;
mod lwe_to_glwe_ksk; pub(crate) use glwe_ct::glwe_encrypt_sk_internal; + +pub const SIGMA: f64 = 3.2; +pub(crate) const SIGMA_BOUND: f64 = 6.0 * SIGMA;
@@ -17,8 +17,8 @@ pub mod layouts;
pub use dist::*; pub use glwe_packing::*; -pub use scratch::*; +pub use encryption::SIGMA; -pub(crate) const SIX_SIGMA: f64 = 6.0; +pub use scratch::*; pub mod tests;
@@ -16,6 +16,7 @@ source::Source,
}; use crate::{ + encryption::SIGMA, layouts::{ GGLWEAutomorphismKey, GLWEPlaintext, GLWESecret, Infos, prepared::{GGLWEAutomorphismKeyPrepared, GLWESecretPrepared, Prepare, PrepareAlloc},@@ -33,7 +34,6 @@ digits: usize,
k_in: usize, k_out: usize, k_apply: usize, - sigma: f64, rank: usize, ) where Module<B>: VecZnxDftAllocBytes@@ -106,7 +106,6 @@ p0,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -117,7 +116,6 @@ p1,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -171,7 +169,7 @@ basek * digits,
0.5, 0.5, 0f64, - sigma * sigma, + SIGMA * SIGMA, 0f64, rank as f64, k_out,@@ -197,7 +195,6 @@ basek: usize,
digits: usize, k_in: usize, k_apply: usize, - sigma: f64, rank: usize, ) where Module<B>: VecZnxDftAllocBytes@@ -283,7 +280,6 @@ p0,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -294,7 +290,6 @@ p1,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -343,7 +338,7 @@ basek * digits,
0.5, 0.5, 0f64, - sigma * sigma, + SIGMA * SIGMA, 0f64, rank as f64, k_in,
@@ -17,6 +17,7 @@ source::Source,
}; use crate::{ + encryption::SIGMA, layouts::{ GGLWEAutomorphismKey, GGLWETensorKey, GGSWCiphertext, GLWESecret, prepared::{GGLWEAutomorphismKeyPrepared, GGLWETensorKeyPrepared, GLWESecretPrepared, Prepare, PrepareAlloc},@@ -35,7 +36,6 @@ k_ksk: usize,
k_tsk: usize, digits: usize, rank: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigAllocBytes@@ -122,7 +122,6 @@ p,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), ); tensor_key.encrypt_sk(@@ -130,7 +129,6 @@ module,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -142,7 +140,6 @@ &pt_scalar,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -171,7 +168,7 @@ basek * digits,
col_j, var_xs, 0f64, - sigma * sigma, + SIGMA * SIGMA, 0f64, rank as f64, k_in,@@ -193,7 +190,6 @@ k_ksk: usize,
k_tsk: usize, digits: usize, rank: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigAllocBytes@@ -277,7 +273,6 @@ p,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), ); tensor_key.encrypt_sk(@@ -285,7 +280,6 @@ module,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -297,7 +291,6 @@ &pt_scalar,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -320,7 +313,7 @@ basek * digits,
col_j, var_xs, 0f64, - sigma * sigma, + SIGMA * SIGMA, 0f64, rank as f64, k_ct,
@@ -16,6 +16,7 @@ source::Source,
}; use crate::{ + encryption::SIGMA, layouts::{ GGLWEAutomorphismKey, GLWECiphertext, GLWEPlaintext, GLWESecret, Infos, prepared::{GGLWEAutomorphismKeyPrepared, GLWESecretPrepared, Prepare, PrepareAlloc},@@ -33,7 +34,6 @@ k_in: usize,
k_ksk: usize, digits: usize, rank: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -114,7 +114,6 @@ p,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -124,7 +123,6 @@ &pt_want,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -140,7 +138,7 @@ basek * digits,
0.5, 0.5, 0f64, - sigma * sigma, + SIGMA * SIGMA, 0f64, rank as f64, k_in,@@ -161,7 +159,6 @@ k_ct: usize,
k_ksk: usize, digits: usize, rank: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -232,7 +229,6 @@ p,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -242,7 +238,6 @@ &pt_want,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -258,7 +253,7 @@ basek * digits,
0.5, 0.5, 0f64, - sigma * sigma, + SIGMA * SIGMA, 0f64, rank as f64, k_ct,
@@ -63,7 +63,6 @@ + TakeVecZnxImpl<B>,
{ let n: usize = module.n(); let basek: usize = 17; - let sigma: f64 = 3.2; let rank: usize = 2;@@ -99,14 +98,7 @@ let mut lwe_pt: LWEPlaintext<Vec<u8>> = LWEPlaintext::alloc(basek, k_lwe_pt);
lwe_pt.encode_i64(data, k_lwe_pt); let mut lwe_ct: LWECiphertext<Vec<u8>> = LWECiphertext::alloc(n_lwe, basek, k_lwe_ct); - lwe_ct.encrypt_sk( - module, - &lwe_pt, - &sk_lwe, - &mut source_xa, - &mut source_xe, - sigma, - ); + lwe_ct.encrypt_sk(module, &lwe_pt, &sk_lwe, &mut source_xa, &mut source_xe); let mut ksk: LWEToGLWESwitchingKey<Vec<u8>> = LWEToGLWESwitchingKey::alloc(n, basek, k_ksk, lwe_ct.size(), rank);@@ -116,7 +108,6 @@ &sk_lwe,
&sk_glwe, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -174,7 +165,6 @@ + TakeVecZnxImpl<B>,
{ let n: usize = module.n(); let basek: usize = 17; - let sigma: f64 = 3.2; let rank: usize = 2;@@ -215,7 +205,6 @@ &glwe_pt,
&sk_glwe_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -227,7 +216,6 @@ &sk_lwe,
&sk_glwe, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );
@@ -15,20 +15,17 @@ },
source::Source, }; -use crate::layouts::{ - GGLWEAutomorphismKey, GLWESecret, - compressed::{Decompress, GGLWEAutomorphismKeyCompressed}, - prepared::{GLWESecretPrepared, PrepareAlloc}, +use crate::{ + encryption::SIGMA, + layouts::{ + GGLWEAutomorphismKey, GLWESecret, + compressed::{Decompress, GGLWEAutomorphismKeyCompressed}, + prepared::{GLWESecretPrepared, PrepareAlloc}, + }, }; -pub fn test_gglwe_automorphisk_key_encrypt_sk<B>( - module: &Module<B>, - basek: usize, - k_ksk: usize, - digits: usize, - rank: usize, - sigma: f64, -) where +pub fn test_gglwe_automorphisk_key_encrypt_sk<B>(module: &Module<B>, basek: usize, k_ksk: usize, digits: usize, rank: usize) +where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B> + VecZnxDftFromVecZnx<B>@@ -94,7 +91,6 @@ p,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -112,7 +108,7 @@ let sk_out_prepared: GLWESecretPrepared<Vec<u8>, B> = sk_out.prepare_alloc(module, scratch.borrow());
atk.key .key - .assert_noise(module, &sk_out_prepared, &sk.data, sigma); + .assert_noise(module, &sk_out_prepared, &sk.data, SIGMA); } pub fn test_gglwe_automorphisk_key_compressed_encrypt_sk<B>(@@ -121,7 +117,6 @@ basek: usize,
k_ksk: usize, digits: usize, rank: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -184,15 +179,7 @@ let p = -5;
let seed_xa: [u8; 32] = [1u8; 32]; - atk_compressed.encrypt_sk( - module, - p, - &sk, - seed_xa, - &mut source_xe, - sigma, - scratch.borrow(), - ); + atk_compressed.encrypt_sk(module, p, &sk, seed_xa, &mut source_xe, scratch.borrow()); let mut sk_out: GLWESecret<Vec<u8>> = sk.clone(); (0..atk_compressed.rank()).for_each(|i| {@@ -211,5 +198,5 @@ atk.decompress(module, &atk_compressed);
atk.key .key - .assert_noise(module, &sk_out_prepared, &sk.data, sigma); + .assert_noise(module, &sk_out_prepared, &sk.data, SIGMA); }
@@ -14,10 +14,13 @@ },
source::Source, }; -use crate::layouts::{ - GGLWESwitchingKey, GLWESecret, - compressed::{Decompress, GGLWESwitchingKeyCompressed}, - prepared::{GLWESecretPrepared, PrepareAlloc}, +use crate::{ + encryption::SIGMA, + layouts::{ + GGLWESwitchingKey, GLWESecret, + compressed::{Decompress, GGLWESwitchingKeyCompressed}, + prepared::{GLWESecretPrepared, PrepareAlloc}, + }, }; pub fn test_gglwe_switching_key_encrypt_sk<B>(@@ -27,7 +30,6 @@ k_ksk: usize,
digits: usize, rank_in: usize, rank_out: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -92,12 +94,11 @@ &sk_in,
&sk_out, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), ); ksk.key - .assert_noise(module, &sk_out_prepared, &sk_in.data, sigma); + .assert_noise(module, &sk_out_prepared, &sk_in.data, SIGMA); } pub fn test_gglwe_switching_key_compressed_encrypt_sk<B>(@@ -107,7 +108,6 @@ k_ksk: usize,
digits: usize, rank_in: usize, rank_out: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -174,7 +174,6 @@ &sk_in,
&sk_out, seed_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -182,5 +181,5 @@ let mut ksk: GGLWESwitchingKey<Vec<u8>> = GGLWESwitchingKey::alloc(n, basek, k_ksk, rows, digits, rank_in, rank_out);
ksk.decompress(module, &ksk_compressed); ksk.key - .assert_noise(module, &sk_out_prepared, &sk_in.data, sigma); + .assert_noise(module, &sk_out_prepared, &sk_in.data, SIGMA); }
@@ -14,13 +14,16 @@ },
source::Source, }; -use crate::layouts::{ - GGSWCiphertext, GLWESecret, - compressed::{Decompress, GGSWCiphertextCompressed}, - prepared::{GLWESecretPrepared, PrepareAlloc}, +use crate::{ + encryption::SIGMA, + layouts::{ + GGSWCiphertext, GLWESecret, + compressed::{Decompress, GGSWCiphertextCompressed}, + prepared::{GLWESecretPrepared, PrepareAlloc}, + }, }; -pub fn test_ggsw_encrypt_sk<B>(module: &Module<B>, basek: usize, k: usize, digits: usize, rank: usize, sigma: f64) +pub fn test_ggsw_encrypt_sk<B>(module: &Module<B>, basek: usize, k: usize, digits: usize, rank: usize) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -89,16 +92,15 @@ &pt_scalar,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), ); - let noise_f = |_col_i: usize| -(k as f64) + sigma.log2() + 0.5; + let noise_f = |_col_i: usize| -(k as f64) + SIGMA.log2() + 0.5; ct.assert_noise(module, &sk_prepared, &pt_scalar, noise_f); } -pub fn test_ggsw_compressed_encrypt_sk<B>(module: &Module<B>, basek: usize, k: usize, digits: usize, rank: usize, sigma: f64) +pub fn test_ggsw_compressed_encrypt_sk<B>(module: &Module<B>, basek: usize, k: usize, digits: usize, rank: usize) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -167,11 +169,10 @@ &pt_scalar,
&sk_prepared, seed_xa, &mut source_xe, - sigma, scratch.borrow(), ); - let noise_f = |_col_i: usize| -(k as f64) + sigma.log2() + 0.5; + let noise_f = |_col_i: usize| -(k as f64) + SIGMA.log2() + 0.5; let mut ct: GGSWCiphertext<Vec<u8>> = GGSWCiphertext::alloc(n, basek, k, rows, digits, rank); ct.decompress(module, &ct_compressed);
@@ -15,6 +15,7 @@ source::Source,
}; use crate::{ + encryption::SIGMA, layouts::{ GLWECiphertext, GLWEPlaintext, GLWEPublicKey, GLWESecret, Infos, compressed::{Decompress, GLWECiphertextCompressed},@@ -23,7 +24,7 @@ },
operations::GLWEOperations, }; -pub fn test_glwe_encrypt_sk<B>(module: &Module<B>, basek: usize, k_ct: usize, k_pt: usize, sigma: f64, rank: usize) +pub fn test_glwe_encrypt_sk<B>(module: &Module<B>, basek: usize, k_ct: usize, k_pt: usize, rank: usize) where Module<B>: VecZnxDftAllocBytes + VecZnxBigAllocBytes@@ -96,7 +97,6 @@ &pt_want,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -105,12 +105,12 @@
pt_want.sub_inplace_ab(module, &pt_have); let noise_have: f64 = pt_want.data.std(basek, 0) * (ct.k() as f64).exp2(); - let noise_want: f64 = sigma; + let noise_want: f64 = SIGMA; assert!(noise_have <= noise_want + 0.2); } -pub fn test_glwe_compressed_encrypt_sk<B>(module: &Module<B>, basek: usize, k_ct: usize, k_pt: usize, sigma: f64, rank: usize) +pub fn test_glwe_compressed_encrypt_sk<B>(module: &Module<B>, basek: usize, k_ct: usize, k_pt: usize, rank: usize) where Module<B>: VecZnxDftAllocBytes + VecZnxBigAllocBytes@@ -187,7 +187,6 @@ &pt_want,
&sk_prepared, seed_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -199,7 +198,7 @@
pt_want.sub_inplace_ab(module, &pt_have); let noise_have: f64 = pt_want.data.std(basek, 0) * (ct.k() as f64).exp2(); - let noise_want: f64 = sigma; + let noise_want: f64 = SIGMA; assert!( noise_have <= noise_want + 0.2,@@ -209,7 +208,7 @@ noise_want + 0.2
); } -pub fn test_glwe_encrypt_zero_sk<B>(module: &Module<B>, basek: usize, k_ct: usize, sigma: f64, rank: usize) +pub fn test_glwe_encrypt_zero_sk<B>(module: &Module<B>, basek: usize, k_ct: usize, rank: usize) where Module<B>: VecZnxDftAllocBytes + VecZnxBigAllocBytes@@ -279,15 +278,14 @@ module,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), ); ct.decrypt(module, &mut pt, &sk_prepared, scratch.borrow()); - assert!((sigma - pt.data.std(basek, 0) * (k_ct as f64).exp2()) <= 0.2); + assert!((SIGMA - pt.data.std(basek, 0) * (k_ct as f64).exp2()) <= 0.2); } -pub fn test_glwe_encrypt_pk<B>(module: &Module<B>, basek: usize, k_ct: usize, k_pk: usize, sigma: f64, rank: usize) +pub fn test_glwe_encrypt_pk<B>(module: &Module<B>, basek: usize, k_ct: usize, k_pk: usize, rank: usize) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -343,7 +341,7 @@ sk.fill_ternary_prob(0.5, &mut source_xs);
let sk_prepared: GLWESecretPrepared<Vec<u8>, B> = sk.prepare_alloc(module, scratch.borrow()); let mut pk: GLWEPublicKey<Vec<u8>> = GLWEPublicKey::alloc(n, basek, k_pk, rank); - pk.generate_from_sk(module, &sk_prepared, &mut source_xa, &mut source_xe, sigma); + pk.generate_from_sk(module, &sk_prepared, &mut source_xa, &mut source_xe); module.vec_znx_fill_uniform(basek, &mut pt_want.data, 0, k_ct, &mut source_xa);@@ -355,7 +353,6 @@ &pt_want,
&pk_prepared, &mut source_xu, &mut source_xe, - sigma, scratch.borrow(), );@@ -364,7 +361,7 @@
pt_want.sub_inplace_ab(module, &pt_have); let noise_have: f64 = pt_want.data.std(basek, 0).log2(); - let noise_want: f64 = ((((rank as f64) + 1.0) * n as f64 * 0.5 * sigma * sigma).sqrt()).log2() - (k_ct as f64); + let noise_want: f64 = ((((rank as f64) + 1.0) * n as f64 * 0.5 * SIGMA * SIGMA).sqrt()).log2() - (k_ct as f64); assert!( noise_have <= noise_want + 0.2,
@@ -14,13 +14,16 @@ },
source::Source, }; -use crate::layouts::{ - GGLWETensorKey, GLWEPlaintext, GLWESecret, Infos, - compressed::{Decompress, GGLWETensorKeyCompressed}, - prepared::{GLWESecretPrepared, PrepareAlloc}, +use crate::{ + encryption::SIGMA, + layouts::{ + GGLWETensorKey, GLWEPlaintext, GLWESecret, Infos, + compressed::{Decompress, GGLWETensorKeyCompressed}, + prepared::{GLWESecretPrepared, PrepareAlloc}, + }, }; -pub fn test_glwe_tensor_key_encrypt_sk<B>(module: &Module<B>, basek: usize, k: usize, sigma: f64, rank: usize) +pub fn test_glwe_tensor_key_encrypt_sk<B>(module: &Module<B>, basek: usize, k: usize, rank: usize) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -87,7 +90,6 @@ module,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -124,14 +126,14 @@
module.vec_znx_sub_scalar_inplace(&mut pt.data, 0, row_i, &sk_ij.data, col_i); let std_pt: f64 = pt.data.std(basek, 0) * (k as f64).exp2(); - assert!((sigma - std_pt).abs() <= 0.5, "{} {}", sigma, std_pt); + assert!((SIGMA - std_pt).abs() <= 0.5, "{} {}", SIGMA, std_pt); }); }); }) }) } -pub fn test_glwe_tensor_key_compressed_encrypt_sk<B>(module: &Module<B>, basek: usize, k: usize, sigma: f64, rank: usize) +pub fn test_glwe_tensor_key_compressed_encrypt_sk<B>(module: &Module<B>, basek: usize, k: usize, rank: usize) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -195,14 +197,7 @@ let sk_prepared: GLWESecretPrepared<Vec<u8>, B> = sk.prepare_alloc(module, scratch.borrow());
let seed_xa: [u8; 32] = [1u8; 32]; - tensor_key_compressed.encrypt_sk( - module, - &sk, - seed_xa, - &mut source_xe, - sigma, - scratch.borrow(), - ); + tensor_key_compressed.encrypt_sk(module, &sk, seed_xa, &mut source_xe, scratch.borrow()); let mut tensor_key: GGLWETensorKey<Vec<u8>> = GGLWETensorKey::alloc(n, basek, k, rows, 1, rank); tensor_key.decompress(module, &tensor_key_compressed);@@ -240,7 +235,7 @@
module.vec_znx_sub_scalar_inplace(&mut pt.data, 0, row_i, &sk_ij.data, col_i); let std_pt: f64 = pt.data.std(basek, 0) * (k as f64).exp2(); - assert!((sigma - std_pt).abs() <= 0.5, "{} {}", sigma, std_pt); + assert!((SIGMA - std_pt).abs() <= 0.5, "{} {}", SIGMA, std_pt); }); }); })
@@ -16,6 +16,7 @@ source::Source,
}; use crate::{ + encryption::SIGMA, layouts::{ GGLWESwitchingKey, GGSWCiphertext, GLWESecret, prepared::{GGSWCiphertextPrepared, GLWESecretPrepared, PrepareAlloc},@@ -33,7 +34,6 @@ k_ggsw: usize,
digits: usize, rank_in: usize, rank_out: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -118,7 +118,6 @@ &sk_in,
&sk_out, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -128,7 +127,6 @@ &pt_rgsw,
&sk_out_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -141,11 +139,11 @@ (0..rank_in).for_each(|i| {
module.vec_znx_rotate_inplace(r as i64, &mut sk_in.data.as_vec_znx_mut(), i); // * X^{r} }); - let var_gct_err_lhs: f64 = sigma * sigma; + let var_gct_err_lhs: f64 = SIGMA * SIGMA; let var_gct_err_rhs: f64 = 0f64; let var_msg: f64 = 1f64 / n as f64; // X^{k} - let var_a0_err: f64 = sigma * sigma; + let var_a0_err: f64 = SIGMA * SIGMA; let var_a1_err: f64 = 1f64 / 12f64; let max_noise: f64 = noise_ggsw_product(@@ -176,7 +174,6 @@ k_ggsw: usize,
digits: usize, rank_in: usize, rank_out: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -259,7 +256,6 @@ &sk_in,
&sk_out, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -269,7 +265,6 @@ &pt_rgsw,
&sk_out_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -282,11 +277,11 @@ (0..rank_in).for_each(|i| {
module.vec_znx_rotate_inplace(r as i64, &mut sk_in.data.as_vec_znx_mut(), i); // * X^{r} }); - let var_gct_err_lhs: f64 = sigma * sigma; + let var_gct_err_lhs: f64 = SIGMA * SIGMA; let var_gct_err_rhs: f64 = 0f64; let var_msg: f64 = 1f64 / n as f64; // X^{k} - let var_a0_err: f64 = sigma * sigma; + let var_a0_err: f64 = SIGMA * SIGMA; let var_a1_err: f64 = 1f64 / 12f64; let max_noise: f64 = noise_ggsw_product(
@@ -16,6 +16,7 @@ source::Source,
}; use crate::{ + encryption::SIGMA, layouts::{ GGSWCiphertext, GLWESecret, prepared::{GGSWCiphertextPrepared, GLWESecretPrepared, PrepareAlloc},@@ -32,7 +33,6 @@ k_out: usize,
k_ggsw: usize, digits: usize, rank: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -113,7 +113,6 @@ &pt_ggsw_rhs,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -123,7 +122,6 @@ &pt_ggsw_lhs,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -133,11 +131,11 @@ ct_ggsw_lhs_out.external_product(module, &ct_ggsw_lhs_in, &ct_rhs_prepared, scratch.borrow());
module.vec_znx_rotate_inplace(k as i64, &mut pt_ggsw_lhs.as_vec_znx_mut(), 0); - let var_gct_err_lhs: f64 = sigma * sigma; + let var_gct_err_lhs: f64 = SIGMA * SIGMA; let var_gct_err_rhs: f64 = 0f64; let var_msg: f64 = 1f64 / n as f64; // X^{k} - let var_a0_err: f64 = sigma * sigma; + let var_a0_err: f64 = SIGMA * SIGMA; let var_a1_err: f64 = 1f64 / 12f64; let max_noise = |_col_j: usize| -> f64 {@@ -167,7 +165,6 @@ k_ct: usize,
k_ggsw: usize, digits: usize, rank: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -248,7 +245,6 @@ &pt_ggsw_rhs,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -258,7 +254,6 @@ &pt_ggsw_lhs,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -268,11 +263,11 @@ ct_ggsw_lhs.external_product_inplace(module, &ct_rhs_prepared, scratch.borrow());
module.vec_znx_rotate_inplace(k as i64, &mut pt_ggsw_lhs.as_vec_znx_mut(), 0); - let var_gct_err_lhs: f64 = sigma * sigma; + let var_gct_err_lhs: f64 = SIGMA * SIGMA; let var_gct_err_rhs: f64 = 0f64; let var_msg: f64 = 1f64 / n as f64; // X^{k} - let var_a0_err: f64 = sigma * sigma; + let var_a0_err: f64 = SIGMA * SIGMA; let var_a1_err: f64 = 1f64 / 12f64; let max_noise = |_col_j: usize| -> f64 {
@@ -15,6 +15,7 @@ source::Source,
}; use crate::{ + encryption::SIGMA, layouts::{ GGSWCiphertext, GLWECiphertext, GLWEPlaintext, GLWESecret, Infos, prepared::{GGSWCiphertextPrepared, GLWESecretPrepared, PrepareAlloc},@@ -31,7 +32,6 @@ k_in: usize,
k_ggsw: usize, digits: usize, rank: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -116,7 +116,6 @@ &pt_rgsw,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -126,7 +125,6 @@ &pt_want,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -136,11 +134,11 @@ ct_glwe_out.external_product(module, &ct_glwe_in, &ct_ggsw_prepared, scratch.borrow());
module.vec_znx_rotate_inplace(k as i64, &mut pt_want.data, 0); - let var_gct_err_lhs: f64 = sigma * sigma; + let var_gct_err_lhs: f64 = SIGMA * SIGMA; let var_gct_err_rhs: f64 = 0f64; let var_msg: f64 = 1f64 / n as f64; // X^{k} - let var_a0_err: f64 = sigma * sigma; + let var_a0_err: f64 = SIGMA * SIGMA; let var_a1_err: f64 = 1f64 / 12f64; let max_noise: f64 = noise_ggsw_product(@@ -168,7 +166,6 @@ k_ct: usize,
k_ggsw: usize, digits: usize, rank: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -243,7 +240,6 @@ &pt_rgsw,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -253,7 +249,6 @@ &pt_want,
&sk_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -263,11 +258,11 @@ ct_glwe.external_product_inplace(module, &ct_ggsw_prepared, scratch.borrow());
module.vec_znx_rotate_inplace(k as i64, &mut pt_want.data, 0); - let var_gct_err_lhs: f64 = sigma * sigma; + let var_gct_err_lhs: f64 = SIGMA * SIGMA; let var_gct_err_rhs: f64 = 0f64; let var_msg: f64 = 1f64 / n as f64; // X^{k} - let var_a0_err: f64 = sigma * sigma; + let var_a0_err: f64 = SIGMA * SIGMA; let var_a1_err: f64 = 1f64 / 12f64; let max_noise: f64 = noise_ggsw_product(
@@ -15,6 +15,7 @@ source::Source,
}; use crate::{ + encryption::SIGMA, layouts::{ GGLWESwitchingKey, GLWESecret, prepared::{GGLWESwitchingKeyPrepared, GLWESecretPrepared, PrepareAlloc},@@ -33,7 +34,6 @@ digits: usize,
rank_in_s0s1: usize, rank_out_s0s1: usize, rank_out_s1s2: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -132,7 +132,6 @@ &sk0,
&sk1, &mut source_xa, &mut source_xe, - sigma, scratch_enc.borrow(), );@@ -143,7 +142,6 @@ &sk1,
&sk2, &mut source_xa, &mut source_xe, - sigma, scratch_enc.borrow(), );@@ -164,7 +162,7 @@ basek * digits,
0.5, 0.5, 0f64, - sigma * sigma, + SIGMA * SIGMA, 0f64, rank_out_s0s1 as f64, k_in,@@ -185,7 +183,6 @@ k_ksk: usize,
digits: usize, rank_in: usize, rank_out: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -269,7 +266,6 @@ &sk0,
&sk1, &mut source_xa, &mut source_xe, - sigma, scratch_enc.borrow(), );@@ -280,7 +276,6 @@ &sk1,
&sk2, &mut source_xa, &mut source_xe, - sigma, scratch_enc.borrow(), );@@ -298,7 +293,7 @@ basek * digits,
var_xs, var_xs, 0f64, - sigma * sigma, + SIGMA * SIGMA, 0f64, rank_out as f64, k_ct,
@@ -16,6 +16,7 @@ source::Source,
}; use crate::{ + encryption::SIGMA, layouts::{ GGLWESwitchingKey, GGLWETensorKey, GGSWCiphertext, GLWESecret, prepared::{GGLWESwitchingKeyPrepared, GGLWETensorKeyPrepared, GLWESecretPrepared, PrepareAlloc},@@ -33,7 +34,6 @@ k_ksk: usize,
k_tsk: usize, digits: usize, rank: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -118,7 +118,6 @@ &sk_in,
&sk_out, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), ); tsk.encrypt_sk(@@ -126,7 +125,6 @@ module,
&sk_out, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -138,7 +136,6 @@ &pt_scalar,
&sk_in_dft, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -160,7 +157,7 @@ basek * digits,
col_j, var_xs, 0f64, - sigma * sigma, + SIGMA * SIGMA, 0f64, rank as f64, k_in,@@ -181,7 +178,6 @@ k_ksk: usize,
k_tsk: usize, digits: usize, rank: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -263,7 +259,6 @@ &sk_in,
&sk_out, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), ); tsk.encrypt_sk(@@ -271,7 +266,6 @@ module,
&sk_out, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -283,7 +277,6 @@ &pt_scalar,
&sk_in_dft, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -299,7 +292,7 @@ basek * digits,
col_j, var_xs, 0f64, - sigma * sigma, + SIGMA * SIGMA, 0f64, rank as f64, k_ct,
@@ -15,6 +15,7 @@ source::Source,
}; use crate::{ + encryption::SIGMA, layouts::{ GGLWESwitchingKey, GLWECiphertext, GLWEPlaintext, GLWESecret, Infos, prepared::{GGLWESwitchingKeyPrepared, GLWESecretPrepared, PrepareAlloc},@@ -32,7 +33,6 @@ k_ksk: usize,
digits: usize, rank_in: usize, rank_out: usize, - sigma: f64, ) where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B>@@ -115,7 +115,6 @@ &sk_in,
&sk_out, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -125,7 +124,6 @@ &pt_want,
&sk_in_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -139,7 +137,7 @@ basek * digits,
0.5, 0.5, 0f64, - sigma * sigma, + SIGMA * SIGMA, 0f64, rank_in as f64, k_in,@@ -149,15 +147,8 @@
ct_out.assert_noise(module, &sk_out_prepared, &pt_want, max_noise + 0.5); } -pub fn test_glwe_keyswitch_inplace<B>( - module: &Module<B>, - basek: usize, - k_ct: usize, - k_ksk: usize, - digits: usize, - rank: usize, - sigma: f64, -) where +pub fn test_glwe_keyswitch_inplace<B>(module: &Module<B>, basek: usize, k_ct: usize, k_ksk: usize, digits: usize, rank: usize) +where Module<B>: VecZnxDftAllocBytes + VecZnxBigNormalize<B> + VecZnxDftFromVecZnx<B>@@ -228,7 +219,6 @@ &sk_in,
&sk_out, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -238,7 +228,6 @@ &pt_want,
&sk_in_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -252,7 +241,7 @@ basek * digits,
0.5, 0.5, 0f64, - sigma * sigma, + SIGMA * SIGMA, 0f64, rank as f64, k_ct,
@@ -62,7 +62,6 @@ + TakeVecZnxImpl<B>,
{ let n: usize = module.n(); let basek: usize = 17; - let sigma: f64 = 3.2; let n_lwe_in: usize = 22; let n_lwe_out: usize = 30;@@ -99,7 +98,6 @@ &lwe_pt_in,
&sk_lwe_in, &mut source_xa, &mut source_xe, - sigma, ); let mut ksk: LWESwitchingKey<Vec<u8>> = LWESwitchingKey::alloc(n, basek, k_ksk, lwe_ct_in.size());@@ -110,7 +108,6 @@ &sk_lwe_in,
&sk_lwe_out, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );
@@ -83,7 +83,6 @@ let basek: usize = 18;
let k_ct: usize = 36; let pt_k: usize = 18; let rank: usize = 3; - let sigma: f64 = 3.2; let digits: usize = 1; let k_ksk: usize = k_ct + basek * digits;@@ -118,7 +117,6 @@ *gal_el,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), ); let atk_prepared: GGLWEAutomorphismKeyPrepared<Vec<u8>, B> = tmp.prepare_alloc(module, scratch.borrow());@@ -137,7 +135,6 @@ &pt,
&sk_dft, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -150,7 +147,6 @@ &pt,
&sk_dft, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );
@@ -19,6 +19,7 @@ source::Source,
}; use crate::{ + encryption::SIGMA, layouts::{ GGLWEAutomorphismKey, GLWECiphertext, GLWEPlaintext, GLWESecret, Infos, prepared::{GGLWEAutomorphismKeyPrepared, GLWESecretPrepared, PrepareAlloc},@@ -26,7 +27,7 @@ },
noise::var_noise_gglwe_product, }; -pub fn test_glwe_trace_inplace<B>(module: &Module<B>, basek: usize, k: usize, sigma: f64, rank: usize) +pub fn test_glwe_trace_inplace<B>(module: &Module<B>, basek: usize, k: usize, rank: usize) where Module<B>: VecZnxDftAllocBytes + VecZnxAutomorphism@@ -110,7 +111,6 @@ &pt_have,
&sk_dft, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -124,7 +124,6 @@ *gal_el,
&sk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), ); let atk_prepared: GGLWEAutomorphismKeyPrepared<Vec<u8>, B> = tmp.prepare_alloc(module, scratch.borrow());@@ -149,13 +148,13 @@ basek,
0.5, 0.5, 1.0 / 12.0, - sigma * sigma, + SIGMA * SIGMA, 0.0, rank as f64, k, k_autokey, ); - noise_want += sigma * sigma * (-2.0 * (k) as f64).exp2(); + noise_want += SIGMA * SIGMA * (-2.0 * (k) as f64).exp2(); noise_want += n as f64 * 1.0 / 12.0 * 0.5 * rank as f64 * (-2.0 * (k) as f64).exp2(); noise_want = noise_want.sqrt().log2();
@@ -26,7 +26,7 @@ println!(
"test_gglwe_switching_key_encrypt_sk digits: {} ranks: ({} {})", di, rank_in, rank_out ); - test_gglwe_switching_key_encrypt_sk(&module, basek, k_ksk, di, rank_in, rank_out, 3.2); + test_gglwe_switching_key_encrypt_sk(&module, basek, k_ksk, di, rank_in, rank_out); }); }); });@@ -46,7 +46,7 @@ println!(
"test_gglwe_switching_key_compressed_encrypt_sk digits: {} ranks: ({} {})", di, rank_in, rank_out ); - test_gglwe_switching_key_compressed_encrypt_sk(&module, basek, k_ksk, di, rank_in, rank_out, 3.2); + test_gglwe_switching_key_compressed_encrypt_sk(&module, basek, k_ksk, di, rank_in, rank_out); }); }); });@@ -79,7 +79,6 @@ di,
rank_in_s0s1, rank_out_s0s1, rank_out_s1s2, - 3.2, ); }) })@@ -102,16 +101,7 @@ println!(
"test_gglwe_switching_key_keyswitch_inplace digits: {} ranks: ({},{})", di, rank_in_s0s1, rank_out_s0s1 ); - test_gglwe_switching_key_keyswitch_inplace( - &module, - basek, - k_ct, - k_ksk, - di, - rank_in_s0s1, - rank_out_s0s1, - 3.2, - ); + test_gglwe_switching_key_keyswitch_inplace(&module, basek, k_ct, k_ksk, di, rank_in_s0s1, rank_out_s0s1); }); }); });@@ -133,9 +123,7 @@ "test_gglwe_switching_key_external_product digits: {} ranks: ({} {})",
di, rank_in, rank_out ); let k_out: usize = k_in; // Better capture noise. - test_gglwe_switching_key_external_product( - &module, basek, k_out, k_in, k_ggsw, di, rank_in, rank_out, 3.2, - ); + test_gglwe_switching_key_external_product(&module, basek, k_out, k_in, k_ggsw, di, rank_in, rank_out); }); }); });@@ -156,7 +144,7 @@ println!(
"test_gglwe_switching_key_external_product_inplace digits: {} ranks: ({} {})", di, rank_in, rank_out ); - test_gglwe_switching_key_external_product_inplace(&module, basek, k_ct, k_ggsw, di, rank_in, rank_out, 3.2); + test_gglwe_switching_key_external_product_inplace(&module, basek, k_ct, k_ggsw, di, rank_in, rank_out); }); }); });@@ -169,14 +157,13 @@ let module: Module<FFT64> = Module::<FFT64>::new(1 << log_n);
let basek: usize = 12; let k: usize = 60; let digits: usize = k.div_ceil(basek) - 1; - let sigma: f64 = 3.2; (1..4).for_each(|rank| { (2..digits + 1).for_each(|di| { println!( "test_gglwe_automorphisk_key_encrypt_sk digits: {} rank: {}", di, rank ); - test_gglwe_automorphisk_key_encrypt_sk(&module, basek, k, di, rank, sigma); + test_gglwe_automorphisk_key_encrypt_sk(&module, basek, k, di, rank); }); }); }@@ -188,14 +175,13 @@ let module: Module<FFT64> = Module::<FFT64>::new(1 << log_n);
let basek: usize = 12; let k: usize = 60; let digits: usize = k.div_ceil(basek) - 1; - let sigma: f64 = 3.2; (1..4).for_each(|rank| { (2..digits + 1).for_each(|di| { println!( "test_gglwe_automorphisk_key_compressed_encrypt_sk digits: {} rank: {}", di, rank ); - test_gglwe_automorphisk_key_compressed_encrypt_sk(&module, basek, k, di, rank, sigma); + test_gglwe_automorphisk_key_compressed_encrypt_sk(&module, basek, k, di, rank); }); }); }@@ -208,7 +194,6 @@ let basek: usize = 12;
let k_in: usize = 60; let k_out: usize = 40; let digits: usize = k_in.div_ceil(basek); - let sigma: f64 = 3.2; (1..4).for_each(|rank| { (2..digits + 1).for_each(|di| { println!(@@ -216,7 +201,7 @@ "test_gglwe_automorphism_key_automorphism: {} rank: {}",
di, rank ); let k_apply: usize = (digits + di) * basek; - test_gglwe_automorphism_key_automorphism(&module, -1, 5, basek, di, k_in, k_out, k_apply, sigma, rank); + test_gglwe_automorphism_key_automorphism(&module, -1, 5, basek, di, k_in, k_out, k_apply, rank); }); }); }@@ -228,7 +213,6 @@ let module: Module<FFT64> = Module::<FFT64>::new(1 << log_n);
let basek: usize = 12; let k_in: usize = 60; let digits: usize = k_in.div_ceil(basek); - let sigma: f64 = 3.2; (1..4).for_each(|rank| { (2..digits + 1).for_each(|di| { println!(@@ -236,7 +220,7 @@ "test_gglwe_automorphism_key_automorphism_inplace: {} rank: {}",
di, rank ); let k_apply: usize = (digits + di) * basek; - test_gglwe_automorphism_key_automorphism_inplace(&module, -1, 5, basek, di, k_in, k_apply, sigma, rank); + test_gglwe_automorphism_key_automorphism_inplace(&module, -1, 5, basek, di, k_in, k_apply, rank); }); }); }@@ -247,7 +231,7 @@ let log_n: usize = 8;
let module: Module<FFT64> = Module::<FFT64>::new(1 << log_n); (1..4).for_each(|rank| { println!("test_glwe_tensor_key_encrypt_sk rank: {}", rank); - test_glwe_tensor_key_encrypt_sk(&module, 16, 54, 3.2, rank); + test_glwe_tensor_key_encrypt_sk(&module, 16, 54, rank); }); }@@ -257,6 +241,6 @@ let log_n: usize = 8;
let module: Module<FFT64> = Module::<FFT64>::new(1 << log_n); (1..4).for_each(|rank| { println!("test_glwe_tensor_key_compressed_encrypt_sk rank: {}", rank); - test_glwe_tensor_key_compressed_encrypt_sk(&module, 16, 54, 3.2, rank); + test_glwe_tensor_key_compressed_encrypt_sk(&module, 16, 54, rank); }); }
@@ -18,7 +18,7 @@ let digits: usize = k_ct / basek;
(1..4).for_each(|rank| { (1..digits + 1).for_each(|di| { println!("test_ggsw_encrypt_sk digits: {} rank: {}", di, rank); - test_ggsw_encrypt_sk(&module, basek, k_ct, di, rank, 3.2); + test_ggsw_encrypt_sk(&module, basek, k_ct, di, rank); }); }); }@@ -36,7 +36,7 @@ println!(
"test_ggsw_compressed_encrypt_sk digits: {} rank: {}", di, rank ); - test_ggsw_compressed_encrypt_sk(&module, basek, k_ct, di, rank, 3.2); + test_ggsw_compressed_encrypt_sk(&module, basek, k_ct, di, rank); }); }); }@@ -54,7 +54,7 @@ let k_ksk: usize = k_in + basek * di;
let k_tsk: usize = k_ksk; println!("test_ggsw_keyswitch digits: {} rank: {}", di, rank); let k_out: usize = k_ksk; // Better capture noise. - test_ggsw_keyswitch(&module, basek, k_out, k_in, k_ksk, k_tsk, di, rank, 3.2); + test_ggsw_keyswitch(&module, basek, k_out, k_in, k_ksk, k_tsk, di, rank); }); }); }@@ -71,7 +71,7 @@ (1..digits + 1).for_each(|di| {
let k_ksk: usize = k_ct + basek * di; let k_tsk: usize = k_ksk; println!("test_ggsw_keyswitch_inplace digits: {} rank: {}", di, rank); - test_ggsw_keyswitch_inplace(&module, basek, k_ct, k_ksk, k_tsk, di, rank, 3.2); + test_ggsw_keyswitch_inplace(&module, basek, k_ct, k_ksk, k_tsk, di, rank); }); }); }@@ -89,7 +89,7 @@ let k_ksk: usize = k_in + basek * di;
let k_tsk: usize = k_ksk; println!("test_ggsw_automorphism rank: {}", rank); let k_out: usize = k_ksk; // Better capture noise. - test_ggsw_automorphism(-5, &module, basek, k_out, k_in, k_ksk, k_tsk, di, rank, 3.2); + test_ggsw_automorphism(-5, &module, basek, k_out, k_in, k_ksk, k_tsk, di, rank); }); }); }@@ -106,7 +106,7 @@ (1..digits + 1).for_each(|di| {
let k_ksk: usize = k_ct + basek * di; let k_tsk: usize = k_ksk; println!("test_ggsw_automorphism_inplace rank: {}", rank); - test_ggsw_automorphism_inplace(-5, &module, basek, k_ct, k_ksk, k_tsk, di, rank, 3.2); + test_ggsw_automorphism_inplace(-5, &module, basek, k_ct, k_ksk, k_tsk, di, rank); }); }); }@@ -123,7 +123,7 @@ (1..digits + 1).for_each(|di| {
let k_ggsw: usize = k_in + basek * di; println!("test external_product digits: {} ranks: {}", di, rank); let k_out: usize = k_in; // Better capture noise. - test_ggsw_external_product(&module, basek, k_in, k_out, k_ggsw, di, rank, 3.2); + test_ggsw_external_product(&module, basek, k_in, k_out, k_ggsw, di, rank); }); }); }@@ -142,7 +142,7 @@ println!(
"test_ggsw_external_product_inplace digits: {} rank: {}", di, rank ); - test_ggsw_external_product_inplace(&module, basek, k_ct, k_ggsw, di, rank, 3.2); + test_ggsw_external_product_inplace(&module, basek, k_ct, k_ggsw, di, rank); }); }); }
@@ -15,7 +15,7 @@ let log_n: usize = 8;
let module: Module<FFT64> = Module::<FFT64>::new(1 << log_n); (1..4).for_each(|rank| { println!("test_glwe_encrypt_sk rank: {}", rank); - test_glwe_encrypt_sk(&module, 8, 54, 30, 3.2, rank); + test_glwe_encrypt_sk(&module, 8, 54, 30, rank); }); }@@ -25,7 +25,7 @@ let log_n: usize = 8;
let module: Module<FFT64> = Module::<FFT64>::new(1 << log_n); (1..4).for_each(|rank| { println!("test_glwe_compressed_encrypt_sk rank: {}", rank); - test_glwe_compressed_encrypt_sk(&module, 8, 54, 30, 3.2, rank); + test_glwe_compressed_encrypt_sk(&module, 8, 54, 30, rank); }); }@@ -35,7 +35,7 @@ let log_n: usize = 8;
let module: Module<FFT64> = Module::<FFT64>::new(1 << log_n); (1..4).for_each(|rank| { println!("test_glwe_encrypt_zero_sk rank: {}", rank); - test_glwe_encrypt_zero_sk(&module, 8, 64, 3.2, rank); + test_glwe_encrypt_zero_sk(&module, 8, 64, rank); }); }@@ -45,7 +45,7 @@ let log_n: usize = 8;
let module: Module<FFT64> = Module::<FFT64>::new(1 << log_n); (1..4).for_each(|rank| { println!("test_glwe_encrypt_pk rank: {}", rank); - test_glwe_encrypt_pk(&module, 8, 64, 64, 3.2, rank) + test_glwe_encrypt_pk(&module, 8, 64, 64, rank) }); }@@ -65,9 +65,7 @@ println!(
"test_glwe_keyswitch digits: {} rank_in: {} rank_out: {}", di, rank_in, rank_out ); - test_glwe_keyswitch( - &module, basek, k_out, k_in, k_ksk, di, rank_in, rank_out, 3.2, - ); + test_glwe_keyswitch(&module, basek, k_out, k_in, k_ksk, di, rank_in, rank_out); }) }); });@@ -84,7 +82,7 @@ (1..4).for_each(|rank| {
(1..digits + 1).for_each(|di| { let k_ksk: usize = k_ct + basek * di; println!("test_glwe_keyswitch_inplace digits: {} rank: {}", di, rank); - test_glwe_keyswitch_inplace(&module, basek, k_ct, k_ksk, di, rank, 3.2); + test_glwe_keyswitch_inplace(&module, basek, k_ct, k_ksk, di, rank); }); }); }@@ -101,7 +99,7 @@ (1..digits + 1).for_each(|di| {
let k_ksk: usize = k_in + basek * di; let k_out: usize = k_ksk; // Better capture noise. println!("test_glwe_automorphism digits: {} rank: {}", di, rank); - test_glwe_automorphism(&module, basek, -5, k_out, k_in, k_ksk, di, rank, 3.2); + test_glwe_automorphism(&module, basek, -5, k_out, k_in, k_ksk, di, rank); }) }); }@@ -120,7 +118,7 @@ println!(
"test_glwe_automorphism_inplace digits: {} rank: {}", di, rank ); - test_glwe_automorphism_inplace(&module, basek, -5, k_ct, k_ksk, di, rank, 3.2); + test_glwe_automorphism_inplace(&module, basek, -5, k_ct, k_ksk, di, rank); }); }); }@@ -137,7 +135,7 @@ (1..digits + 1).for_each(|di| {
let k_ggsw: usize = k_in + basek * di; let k_out: usize = k_ggsw; // Better capture noise println!("test_glwe_external_product digits: {} rank: {}", di, rank); - test_glwe_external_product(&module, basek, k_out, k_in, k_ggsw, di, rank, 3.2); + test_glwe_external_product(&module, basek, k_out, k_in, k_ggsw, di, rank); }); }); }@@ -156,7 +154,7 @@ println!(
"test_glwe_external_product_inplace digits: {} rank: {}", di, rank ); - test_glwe_external_product_inplace(&module, basek, k_ct, k_ggsw, di, rank, 3.2); + test_glwe_external_product_inplace(&module, basek, k_ct, k_ggsw, di, rank); }); }); }@@ -167,7 +165,7 @@ let log_n: usize = 8;
let module: Module<FFT64> = Module::<FFT64>::new(1 << log_n); (1..4).for_each(|rank| { println!("test_glwe_trace_inplace rank: {}", rank); - test_glwe_trace_inplace(&module, 8, 54, 3.2, rank); + test_glwe_trace_inplace(&module, 8, 54, rank); }); }
@@ -0,0 +1,87 @@
+# 🐙 Poulpy-HAL + +**Poulpy-HAL** is a Rust crate that provide backend agnostic layouts and trait-based low-level lattice arithmetic matching the API of [**spqlios-arithmetic**](https://github.com/tfhe/spqlios-arithmetic). This allows developpers to implement lattice-based schemes generically, with the ability to plug in any optimized backends (e.g. CPU, GPU, FPGA) at runtime. + +## Crate Organization + +### **poulpy-hal/layouts** + +This module defines backend agnostic layouts following **spqlios-arithmetic** types. There are two main types to distinguish from: user facing types and backend types. User facing types, such as `vec_znx`, serve both as the input and output of computations, while backend types, such as `svp_ppol` (a.k.a. scalar vector product prepared polynomial), are pre-processed write-only types stored in a backend-specific representation for optimized evaluation. For example in FFT64 AVX2 CPU implementation, an `svp_ppol`, which is the prepared type of `scalar_znx`, is stored in DFT with an AVX optimized data ordering. + +This module also provide various helpers over these types, as well as serialization for the front end types `scalar_znx`, `vec_znx` and `mat_znx`. + +#### Module + +The `module` is a struct that stores backend-specific pre-computations (e.g. DFT tables). + +#### ScalarZnx + +A `scalar_znx` is a front-end generic and backend agnostic type that stores a single small polynomial of `i64` coefficients. This type is mainly used to store secret-keys or small plaintext polynomials (for example GGSW plaintexts). + +#### VecZnx + +A `vec_znx` is a front-end generic and backend agnostic type that stores a vector of small polynomials (i.e. a vector of scalars). Each polynomial is a `limb` that provides an additional `basek`-bits of precision in the Torus. For example a `vec_znx` with `n`=1024 `basek`=2 with 3 limbs can store 1024 coefficients with 36 bits of precision in the torus. In practice this type is used for LWE and GLWE ciphertexts/plaintexts. + + +#### VecZnxDft + +A `vec_znx_dft` is a backend-specific type that stores a `vec_znx` in the DFT domain. + + +#### VecZnxBig + +A `vec_znx_big` is a backend-specific type that stores a `vec_znx` with big coefficient, for example the result of a scalar multiplication or a polynomial convolution. It can be mapped back to a `vec_znx` by applying a normalization step. + + +#### MatZnx + +A `mat_znx` is a front-end generic and backend agnostic type that stores a matrix of small polynomials (i.e. a matrix of scalars). Each row of the matrix is a `vec_znx`. In practice this type is used for GGLWE and GGSW ciphertexts/plaintexts. + + +#### SvpPPol + +An `svp_ppol` (scalar vector prepared polynomial) is a backend-specific type that stores a prepared `scalar_znx`. It is used to perform a scalar vector product which multiplies a `vec_znx` by a `scalar_znx`, typically when multiplying with a secret-key. + +#### VmpPMat + +A `vmp_pmat` (vector matrix product prepared matrix) is a backend-specific type that stores a prepared `mat_znx`. It is used to perform a vector matrix product which multiplies a `vec_znx` by a `mat_znx`, a typical step of the GLWE gadget product. + +#### Scratch + +A `scratch` is a backend agnostic scratch space manager which allows to borrows bytes or structs for intermediate computations. + +--------- + +### **poulpy-hal/api** + +This module provides the user facing traits-based API of the hardware acceleration layer. These are the traits used to implement **`poulpy-core`** and **`poulpy-schemes`**. These currently include the `module` instantiation, arithmetic over `vec_znx`, `vec_znx_big`, `vec_znx_dft`, `svp_ppol`, `vmp_pmat` and scratch space management. + + +--------- + +### **poulpy-hal/oep** + +This module provides open extension points, that can be implemented to provide a concrete backend to crates implementing lattice-based arithmetic using **`poulpy-hal/api`** and **`poulpy-hal/layouts`**, such as **`poulpy-core`** and **`poulpy-schemes`** or any other project/application. + + +--------- + +### **poulpy-hal/delegates** + +This module provides a link between the open extension points and public API. + + +--------- + +### Pipeline Example + +```mermaid +flowchart TD + A[VecZnx] -->|DFT|B[VecZnxDft]-->E + C[ScalarZnx] -->|prepare|D[SvpPPol]-->E + E{SvpApply}-->VecZnxDft-->|IDFT|VecZnxBig-->|Normalize|VecZnx +``` + +## Testing + +A full generic and backend agnostic testing suit for the layouts and public API is planned. This will allow to test the correctness of any backend easily.
@@ -10,8 +10,8 @@ homepage = "https://github.com/phantomzone-org/poulpy"
documentation = "https://docs.rs/poulpy" [dependencies] -poulpy-backend = "0.1.2" -poulpy-hal = "0.1.2" -poulpy-core = "0.1.1" +poulpy-backend = {path="../poulpy-backend"} +poulpy-hal = {path="../poulpy-hal"} +poulpy-core = {path="../poulpy-core"} itertools = "0.14.0" byteorder = "1.5.0"
@@ -0,0 +1,12 @@
+# 🐙 Poulpy-Schemes + +**Poulpy-Schemes** is a Rust crate built on **`poulpy-hal`** and **`poulpy-core`**, providing backend agnostic RLWE-based FHE schemes implementation. + +## Getting Started + +See [./examples/circuit_bootstrapping.rs](./examples/circuit_bootstrapping.rs) + +## Available Schemes + +- TFHE/FHEW: under construction. Blind rotation & circuit bootstrapping implemented. +- CKKS: planned
@@ -38,9 +38,6 @@
// GLWE rank let rank: usize = 1; - // Noise (discrete) standard deviation - let sigma: f64 = 3.2; - // LWE degree let n_lwe: usize = 574;@@ -110,22 +107,16 @@ let mut pt_lwe: LWEPlaintext<Vec<u8>> = LWEPlaintext::alloc(basek, k_lwe_pt);
// LWE plaintext(data * 2^{- (k_lwe_pt - 1)}) pt_lwe.encode_i64(data, k_lwe_pt + 1); // +1 for padding bit + + // Normalize plaintext to nicely print coefficients module.vec_znx_normalize_inplace(basek, pt_lwe.data_mut(), 0, scratch.borrow()); - println!("pt_lwe: {}", pt_lwe); // LWE ciphertext let mut ct_lwe: LWECiphertext<Vec<u8>> = LWECiphertext::alloc(n_lwe, basek, k_lwe_ct); // Encrypt LWE Plaintext - ct_lwe.encrypt_sk( - &module, - &pt_lwe, - &sk_lwe, - &mut source_xa, - &mut source_xe, - sigma, - ); + ct_lwe.encrypt_sk(&module, &pt_lwe, &sk_lwe, &mut source_xa, &mut source_xe); let now: Instant = Instant::now();@@ -143,7 +134,6 @@ k_tsk,
rows_tsk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), ); println!("CBT-KGEN: {} ms", now.elapsed().as_millis());@@ -199,7 +189,6 @@ &pt_glwe,
&sk_glwe_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );
@@ -71,7 +71,6 @@ sk_glwe: &GLWESecretPrepared<DataSkGLWE, B>,
sk_lwe: &LWESecret<DataSkLWE>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where DataSkGLWE: DataRef,@@ -100,7 +99,7 @@ let sk_ref: ScalarZnx<&[u8]> = sk_lwe.data().to_ref();
self.keys.iter_mut().enumerate().for_each(|(i, ggsw)| { pt.at_mut(0, 0)[0] = sk_ref.at(0, 0)[i]; - ggsw.encrypt_sk(module, &pt, sk_glwe, source_xa, source_xe, sigma, scratch); + ggsw.encrypt_sk(module, &pt, sk_glwe, source_xa, source_xe, scratch); }); } }@@ -157,7 +156,6 @@ sk_glwe: &GLWESecretPrepared<DataSkGLWE, B>,
sk_lwe: &LWESecret<DataSkLWE>, seed_xa: [u8; 32], source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where DataSkGLWE: DataRef,@@ -209,7 +207,6 @@ &pt,
sk_glwe, source_xa.new_seed(), source_xe, - sigma, scratch, ); });
@@ -28,7 +28,6 @@ sk_glwe: &GLWESecretPrepared<DataSkGLWE, B>,
sk_lwe: &LWESecret<DataSkLWE>, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) where DataSkGLWE: DataRef,
@@ -127,7 +127,6 @@ &sk_glwe_dft,
&sk_lwe, &mut source_xa, &mut source_xe, - 3.2, scratch.borrow(), );@@ -139,14 +138,7 @@ let x: i64 = 15 % (message_modulus as i64);
pt_lwe.encode_i64(x, log_message_modulus + 1); - lwe.encrypt_sk( - module, - &pt_lwe, - &sk_lwe, - &mut source_xa, - &mut source_xe, - 3.2, - ); + lwe.encrypt_sk(module, &pt_lwe, &sk_lwe, &mut source_xa, &mut source_xe); let f = |x: i64| -> i64 { 2 * x + 1 };
@@ -35,7 +35,6 @@ k_tsk: usize,
rows_tsk: usize, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) -> Self where@@ -88,7 +87,6 @@ k_tsk: usize,
rows_tsk: usize, source_xa: &mut Source, source_xe: &mut Source, - sigma: f64, scratch: &mut Scratch<B>, ) -> Self where@@ -101,9 +99,7 @@ let gal_els: Vec<i64> = GLWECiphertext::trace_galois_elements(module);
gal_els.iter().for_each(|gal_el| { let mut key: GGLWEAutomorphismKey<Vec<u8>> = GGLWEAutomorphismKey::alloc(sk_glwe.n(), basek, k_trace, rows_trace, 1, sk_glwe.rank()); - key.encrypt_sk( - module, *gal_el, sk_glwe, source_xa, source_xe, sigma, scratch, - ); + key.encrypt_sk(module, *gal_el, sk_glwe, source_xa, source_xe, scratch); auto_keys.insert(*gal_el, key); });@@ -124,12 +120,11 @@ &sk_glwe_prepared,
sk_lwe, source_xa, source_xe, - sigma, scratch, ); let mut tsk: GGLWETensorKey<Vec<u8>> = GGLWETensorKey::alloc(sk_glwe.n(), basek, k_tsk, rows_tsk, 1, sk_glwe.rank()); - tsk.encrypt_sk(module, sk_glwe, source_xa, source_xe, sigma, scratch); + tsk.encrypt_sk(module, sk_glwe, source_xa, source_xe, scratch); Self { brk,
@@ -101,7 +101,6 @@ let n: usize = module.n();
let basek: usize = 17; let extension_factor: usize = 1; let rank: usize = 1; - let sigma: f64 = 3.2; let n_lwe: usize = 77; let k_lwe_pt: usize = 4;@@ -139,14 +138,7 @@
println!("pt_lwe: {}", pt_lwe); let mut ct_lwe: LWECiphertext<Vec<u8>> = LWECiphertext::alloc(n_lwe, basek, k_lwe_ct); - ct_lwe.encrypt_sk( - module, - &pt_lwe, - &sk_lwe, - &mut source_xa, - &mut source_xe, - sigma, - ); + ct_lwe.encrypt_sk(module, &pt_lwe, &sk_lwe, &mut source_xa, &mut source_xe); let now: Instant = Instant::now(); let cbt_key: CircuitBootstrappingKey<Vec<u8>, BRA> = CircuitBootstrappingKey::encrypt_sk(@@ -162,7 +154,6 @@ k_tsk,
rows_tsk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), ); println!("CBT-KGEN: {} ms", now.elapsed().as_millis());@@ -207,7 +198,6 @@ &pt_glwe,
&sk_glwe_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );@@ -285,11 +275,10 @@ BlindRotationKey<Vec<u8>, BRA>: PrepareAlloc<B, BlindRotationKeyPrepared<Vec<u8>, BRA, B>>,
BlindRotationKeyPrepared<Vec<u8>, BRA, B>: BlincRotationExecute<B>, BlindRotationKey<Vec<u8>, BRA>: BlindRotationKeyAlloc + BlindRotationKeyEncryptSk<B>, { - let n = module.n(); + let n: usize = module.n(); let basek: usize = 14; let extension_factor: usize = 1; let rank: usize = 2; - let sigma: f64 = 3.2; let n_lwe: usize = 77; let k_lwe_pt: usize = 1;@@ -327,14 +316,7 @@
println!("pt_lwe: {}", pt_lwe); let mut ct_lwe: LWECiphertext<Vec<u8>> = LWECiphertext::alloc(n_lwe, basek, k_lwe_ct); - ct_lwe.encrypt_sk( - module, - &pt_lwe, - &sk_lwe, - &mut source_xa, - &mut source_xe, - sigma, - ); + ct_lwe.encrypt_sk(module, &pt_lwe, &sk_lwe, &mut source_xa, &mut source_xe); let now: Instant = Instant::now(); let cbt_key: CircuitBootstrappingKey<Vec<u8>, BRA> = CircuitBootstrappingKey::encrypt_sk(@@ -350,7 +332,6 @@ k_tsk,
rows_tsk, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), ); println!("CBT-KGEN: {} ms", now.elapsed().as_millis());@@ -391,7 +372,6 @@ &pt_glwe,
&sk_glwe_prepared, &mut source_xa, &mut source_xe, - sigma, scratch.borrow(), );