diff --git a/nb/powder.ipynb b/nb/powder.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..82bd3bdc14f9eb7a5a805ad762e359637aebaa3b
--- /dev/null
+++ b/nb/powder.ipynb
@@ -0,0 +1,164 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c134ff02-0eba-4146-803b-2b69f9ccaef9",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "runno = 189\n",
+    "propno = 900463\n",
+    "\n",
+    "ref_geom_fn = \"/gpfs/exfel/exp/SPB/202430/p900429/usr/Shared/geom/p5681_r64_67_refined.geom\"  # initial geometry file name"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0bdf665e-9ad7-4a40-a59e-23b428312f4f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# !geomtools-powder-center -g {ref_geom_fn} -c AgBh ../../../nb/powder_sum_p{propno:06d}_r{runno:04d}.h5"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "504eca33-7f3d-4b7e-9ce1-9edcadff6387",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import os\n",
+    "import numpy as np\n",
+    "import matplotlib.pyplot as plt\n",
+    "import h5py\n",
+    "\n",
+    "from extra_data import open_run\n",
+    "from extra_geom import agipd_asic_seams, AGIPD_1MGeometry\n",
+    "from extra_geom.motors import AGIPD_1MMotors\n",
+    "from extra.components import AGIPD1MQuadrantMotors\n",
+    "\n",
+    "from geomtools.powder.calibrants import get_calibrant\n",
+    "from geomtools.powder.misc import agipd_borders\n",
+    "from geomtools.powder import PowderDiffraction"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "33f4f527-d61e-45c1-8301-1be7c6aa8edf",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fn = f\"powder_sum_p{propno:06d}_r{runno:04d}.h5\"\n",
+    "if not os.path.exists(fn):\n",
+    "    !srun -p exfel geomtools-powder-sum -p {propno} -r {runno} -D ^scratch/pycal/proc -m ^usr/Shared/mask/mask_hvoff_20240807.h5 -l agipd1m SPB_DET_AGIPD1M-1"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e9c147f0-e011-46cb-bf0f-e0ef01a6d8a4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "with h5py.File(fn, \"r\") as f:\n",
+    "    # conditions\n",
+    "    clen = f[\"powderSum/conditions/cameraLen\"][()]\n",
+    "    photon_en = f[\"powderSum/conditions/photonEnergy\"][()]\n",
+    "    lmd = f[\"powderSum/conditions/waveLength\"][()]\n",
+    "    detector_id = f[\"powderSum/conditions/detectorId\"][()].decode()\n",
+    "\n",
+    "    # average image\n",
+    "    img_mean = f[\"powderSum/image/mean\"][:]\n",
+    "    img_std = f[\"powderSum/image/std\"][:]\n",
+    "    img_count = f[\"powderSum/image/count\"][:]\n",
+    "    num_frames = f[\"powderSum/image/numFrames\"][:]\n",
+    "    mask = f[\"powderSum/image/mask\"][:].astype(bool)\n",
+    "    modules = f[\"powderSum/image/modules\"][:]\n",
+    "\n",
+    "print(\"Photon energy\", photon_en, \"(keV)\")\n",
+    "print(\"Wave length\", lmd, \"(nm)\")\n",
+    "print(\"Camera length\", clen, \"(m)\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8fdf43d7-db13-4493-a030-3132337e1512",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# geometry\n",
+    "run = open_run(propno, runno, data=\"raw\")\n",
+    "ref_geom = AGIPD_1MGeometry.from_crystfel_geom(ref_geom_fn)\n",
+    "tracker = AGIPD_1MMotors(ref_geom)\n",
+    "motors = AGIPD1MQuadrantMotors(run)\n",
+    "geom = tracker.geom_at(motors.most_frequent_positions())"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "332a77f6-b3b7-4e13-b6e4-5838391949df",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "img = np.copy(img_mean)\n",
+    "geom.plot_data(img, norm=\"log\", interpolation=\"none\")\n",
+    "plt.show()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "83dd3cb1-00d5-4337-80c5-e83d9eeec0b1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "rings = get_calibrant(\"AgBh\")\n",
+    "borders = agipd_borders()\n",
+    "pw = PowderDiffraction(geom, rings, clen, lmd,\n",
+    "                       border_mask=~borders, make_shadow_mask=True)\n",
+    "pw.fit(img_mean, mask, num_frames)\n",
+    "pw.refinement_info()\n",
+    "pw.draw()\n",
+    "plt.show()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1bd08493-2562-4f92-82aa-eb866d5af791",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "pw.draw_radial_profile(img_mean, mask)\n",
+    "plt.show()"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "egor-dev",
+   "language": "python",
+   "name": "egor-dev"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.16"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/pyproject.toml b/pyproject.toml
index 60b973558f07a45af624b83dcf4fa25f3c13050e..b3af3bac3079f0818267941ca7f85a3dcfe213b0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,3 +31,5 @@ dependencies = [
 [project.scripts]
 geomtools-push = "geomtools.sfx.report:push_geometry"
 geomtools-refine-sfx = "geomtools.sfx.optimiser:refine"
+geomtools-powder-sum = "geomtools.powder.powdersum:main"
+geomtools-powder-center = "geomtools.powder.find_center:main"
diff --git a/src/geomtools/powder/__init__.py b/src/geomtools/powder/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..5cfdbda813573a20b544614d4b63245b2712d497
--- /dev/null
+++ b/src/geomtools/powder/__init__.py
@@ -0,0 +1,2 @@
+from .image_tools import make_shadow_mask  # noqa E401
+from .powder import PowderDiffraction  # noqa E401
diff --git a/src/geomtools/powder/calibrants/AgBh.D b/src/geomtools/powder/calibrants/AgBh.D
new file mode 100644
index 0000000000000000000000000000000000000000..48cc5a2fd44574279b29757b8a40584d3084e166
--- /dev/null
+++ b/src/geomtools/powder/calibrants/AgBh.D
@@ -0,0 +1,52 @@
+# Calibrant: Silver Behenate (AgBh)
+# Pseudocrystal a=inf b=inf c=58.380
+# Ref: doi:10.1107/S0021889899012388
+5.83800000e+01 # (0,0,1)
+2.91900000e+01 # (0,0,2)
+1.94600000e+01 # (0,0,3)
+1.45950000e+01 # (0,0,4)
+1.16760000e+01 # (0,0,5)
+9.73000000e+00 # (0,0,6)
+8.34000000e+00 # (0,0,7)
+7.29750000e+00 # (0,0,8)
+6.48666667e+00 # (0,0,9)
+5.83800000e+00 # (0,0,10)
+5.30727273e+00 # (0,0,11)
+4.86500000e+00 # (0,0,12)
+4.49076923e+00 # (0,0,13)
+4.17000000e+00 # (0,0,14)
+3.89200000e+00 # (0,0,15)
+3.64875000e+00 # (0,0,16)
+3.43411765e+00 # (0,0,17)
+3.24333333e+00 # (0,0,18)
+3.07263158e+00 # (0,0,19)
+2.91900000e+00 # (0,0,20)
+2.78000000e+00 # (0,0,21)
+2.65363636e+00 # (0,0,22)
+2.53826087e+00 # (0,0,23)
+2.43250000e+00 # (0,0,24)
+2.33520000e+00 # (0,0,25)
+2.24538462e+00 # (0,0,26)
+2.16222222e+00 # (0,0,27)
+2.08500000e+00 # (0,0,28)
+2.01310345e+00 # (0,0,29)
+1.94600000e+00 # (0,0,30)
+1.88322581e+00 # (0,0,31)
+1.82437500e+00 # (0,0,32)
+1.76909091e+00 # (0,0,33)
+1.71705882e+00 # (0,0,34)
+1.66800000e+00 # (0,0,35)
+1.62166667e+00 # (0,0,36)
+1.57783784e+00 # (0,0,37)
+1.53631579e+00 # (0,0,38)
+1.49692308e+00 # (0,0,39)
+1.45950000e+00 # (0,0,40)
+1.42390244e+00 # (0,0,41)
+1.39000000e+00 # (0,0,42)
+1.35767442e+00 # (0,0,43)
+1.32681818e+00 # (0,0,44)
+1.29733333e+00 # (0,0,45)
+1.26913043e+00 # (0,0,46)
+1.24212766e+00 # (0,0,47)
+1.21625000e+00 # (0,0,48)
+1.19142857e+00 # (0,0,49)
diff --git a/src/geomtools/powder/calibrants/LaB6.D b/src/geomtools/powder/calibrants/LaB6.D
new file mode 100644
index 0000000000000000000000000000000000000000..ffaf5ba742f1ac776cd6309f1195c0369dbd3af0
--- /dev/null
+++ b/src/geomtools/powder/calibrants/LaB6.D
@@ -0,0 +1,1444 @@
+# Calibrant: Lanthanum hexaboride (LaB6)
+# Primitive cubic cell a=4.1568 b=4.1568 c=4.1568 alpha=90.000 beta=90.000 gamma=90.000
+# Ref: NIST standard reference material 660c
+4.15682600 # (1, 0, 0) 6
+2.93931985 # (1, 1, 0) 12
+2.39994461 # (1, 1, 1) 8
+2.07841300 # (2, 0, 0) 6
+1.85898910 # (2, 1, 0) 24
+1.69701711 # (2, 1, 1) 24
+1.46965993 # (2, 2, 0) 12
+1.38560867 # (3, 0, 0) 30
+1.31450380 # (3, 1, 0) 24
+1.25333020 # (3, 1, 1) 24
+1.19997231 # (2, 2, 2) 8
+1.15289610 # (3, 2, 0) 24
+1.11095848 # (3, 2, 1) 48
+1.03920650 # (4, 0, 0) 6
+1.00817839 # (4, 1, 0) 48
+0.97977328 # (4, 1, 1) 36
+0.95364129 # (3, 3, 1) 24
+0.92949455 # (4, 2, 0) 24
+0.90709380 # (4, 2, 1) 48
+0.88623828 # (3, 3, 2) 24
+0.84850855 # (4, 2, 2) 24
+0.83136520 # (5, 0, 0) 30
+0.81522065 # (5, 1, 0) 72
+0.79998154 # (5, 1, 1) 32
+0.77190321 # (5, 2, 0) 72
+0.75892912 # (5, 2, 1) 48
+0.73482996 # (4, 4, 0) 12
+0.72361053 # (5, 2, 2) 48
+0.71288978 # (5, 3, 0) 48
+0.70263184 # (5, 3, 1) 48
+0.69280433 # (6, 0, 0) 30
+0.68337798 # (6, 1, 0) 24
+0.67432622 # (6, 1, 1) 72
+0.65725190 # (6, 2, 0) 24
+0.64918715 # (6, 2, 1) 96
+0.64141218 # (5, 4, 1) 48
+0.63391002 # (5, 3, 3) 24
+0.62666510 # (6, 2, 2) 24
+0.61966303 # (6, 3, 0) 72
+0.61289056 # (6, 3, 1) 48
+0.59998615 # (4, 4, 4) 8
+0.59383229 # (7, 0, 0) 54
+0.58786397 # (7, 1, 0) 84
+0.58207207 # (7, 1, 1) 48
+0.57644805 # (6, 4, 0) 24
+0.57098396 # (7, 2, 0) 72
+0.56567237 # (7, 2, 1) 96
+0.55547924 # (6, 4, 2) 48
+0.55058505 # (7, 2, 2) 48
+0.54581799 # (7, 3, 0) 24
+0.54117265 # (7, 3, 1) 72
+0.53222703 # (6, 5, 0) 72
+0.52791743 # (7, 3, 2) 96
+0.51960325 # (8, 0, 0) 6
+0.51559081 # (8, 1, 0) 96
+0.51166991 # (8, 1, 1) 96
+0.50783712 # (7, 3, 3) 24
+0.50408920 # (8, 2, 0) 48
+0.50042304 # (8, 2, 1) 96
+0.49683574 # (6, 5, 3) 48
+0.48988664 # (8, 2, 2) 36
+0.48651968 # (8, 3, 0) 48
+0.48322121 # (8, 3, 1) 120
+0.47998892 # (7, 5, 1) 56
+0.47682064 # (6, 6, 2) 24
+0.47371429 # (8, 3, 2) 96
+0.47066786 # (7, 5, 2) 48
+0.46474728 # (8, 4, 0) 24
+0.46186956 # (9, 0, 0) 102
+0.45904464 # (9, 1, 0) 48
+0.45627093 # (9, 1, 1) 72
+0.45354690 # (8, 4, 2) 48
+0.45087108 # (9, 2, 0) 48
+0.44824208 # (9, 2, 1) 120
+0.44311914 # (6, 6, 4) 24
+0.44062267 # (9, 2, 2) 144
+0.43816793 # (9, 3, 0) 120
+0.43575377 # (9, 3, 1) 48
+0.43104278 # (8, 5, 2) 48
+0.42874387 # (9, 3, 2) 96
+0.42425428 # (8, 4, 4) 24
+0.42206173 # (9, 4, 0) 48
+0.41990284 # (9, 4, 1) 108
+0.41777673 # (9, 3, 3) 72
+0.41568260 # (10, 0, 0) 30
+0.41361965 # (10, 1, 0) 168
+0.41158711 # (10, 1, 1) 48
+0.40761032 # (10, 2, 0) 72
+0.40566468 # (10, 2, 1) 96
+0.40374663 # (9, 5, 0) 72
+0.40185554 # (9, 5, 1) 72
+0.39999077 # (10, 2, 2) 32
+0.39815172 # (10, 3, 0) 72
+0.39633781 # (10, 3, 1) 144
+0.39104130 # (10, 3, 2) 96
+0.38932243 # (8, 7, 1) 96
+0.38762602 # (9, 5, 3) 48
+0.38595160 # (10, 4, 0) 72
+0.38429870 # (10, 4, 1) 120
+0.38266685 # (10, 3, 3) 72
+0.37946456 # (10, 4, 2) 48
+0.37789327 # (11, 0, 0) 78
+0.37634134 # (11, 1, 0) 120
+0.37480838 # (11, 1, 1) 48
+0.37179782 # (11, 2, 0) 144
+0.37031949 # (11, 2, 1) 144
+0.36741498 # (8, 8, 0) 12
+0.36598812 # (11, 2, 2) 144
+0.36457776 # (11, 3, 0) 48
+0.36318357 # (11, 3, 1) 120
+0.36180526 # (10, 4, 4) 48
+0.36044253 # (9, 6, 4) 48
+0.35909507 # (11, 3, 2) 168
+0.35644489 # (10, 6, 0) 48
+0.35514161 # (11, 4, 0) 96
+0.35385253 # (11, 4, 1) 96
+0.35257738 # (11, 3, 3) 72
+0.35131592 # (10, 6, 2) 48
+0.35006790 # (11, 4, 2) 96
+0.34883309 # (9, 6, 5) 48
+0.34640217 # (12, 0, 0) 30
+0.34520561 # (12, 1, 0) 96
+0.34402137 # (12, 1, 1) 192
+0.34284923 # (11, 5, 1) 56
+0.34168899 # (12, 2, 0) 24
+0.34054045 # (12, 2, 1) 168
+0.33940342 # (11, 5, 2) 120
+0.33716311 # (12, 2, 2) 72
+0.33605946 # (12, 3, 0) 144
+0.33496658 # (12, 3, 1) 96
+0.33388430 # (11, 5, 3) 96
+0.33175083 # (12, 3, 2) 72
+0.33069932 # (11, 6, 1) 96
+0.32862595 # (12, 4, 0) 24
+0.32760378 # (12, 4, 1) 192
+0.32659109 # (12, 3, 3) 108
+0.32558774 # (9, 9, 1) 24
+0.32459358 # (12, 4, 2) 96
+0.32360847 # (10, 8, 1) 96
+0.32263227 # (11, 6, 3) 120
+0.32070609 # (10, 8, 2) 48
+0.31975585 # (13, 0, 0) 78
+0.31881400 # (13, 1, 0) 144
+0.31788043 # (13, 1, 1) 120
+0.31695501 # (10, 6, 6) 24
+0.31603763 # (13, 2, 0) 168
+0.31512817 # (13, 2, 1) 144
+0.31333255 # (12, 4, 4) 24
+0.31244618 # (13, 2, 2) 48
+0.31156728 # (13, 3, 0) 96
+0.31069576 # (13, 3, 1) 120
+0.30983152 # (12, 6, 0) 72
+0.30897444 # (12, 6, 1) 120
+0.30812444 # (13, 3, 2) 144
+0.30644528 # (12, 6, 2) 48
+0.30561593 # (13, 4, 0) 192
+0.30479327 # (13, 4, 1) 144
+0.30397722 # (13, 3, 3) 48
+0.30236460 # (13, 4, 2) 192
+0.30156785 # (10, 9, 3) 48
+0.29999308 # (8, 8, 8) 8
+0.29921488 # (12, 7, 0) 48
+0.29844271 # (13, 5, 0) 240
+0.29767649 # (13, 5, 1) 96
+0.29691614 # (14, 0, 0) 54
+0.29616159 # (14, 1, 0) 120
+0.29541276 # (14, 1, 1) 120
+0.29393199 # (14, 2, 0) 84
+0.29319990 # (14, 2, 1) 144
+0.29247326 # (12, 7, 3) 72
+0.29175199 # (13, 5, 3) 96
+0.29103603 # (14, 2, 2) 48
+0.29032532 # (14, 3, 0) 96
+0.28961979 # (14, 3, 1) 240
+0.28822402 # (12, 8, 0) 24
+0.28753367 # (14, 3, 2) 240
+0.28684825 # (13, 5, 4) 96
+0.28616770 # (11, 9, 3) 72
+0.28549198 # (14, 4, 0) 72
+0.28482102 # (14, 4, 1) 96
+0.28415478 # (14, 3, 3) 72
+0.28283618 # (14, 4, 2) 96
+0.28218374 # (12, 8, 3) 96
+0.28153578 # (13, 7, 0) 120
+0.28089227 # (13, 7, 1) 96
+0.27961838 # (14, 5, 0) 192
+0.27898789 # (14, 5, 1) 144
+0.27773962 # (12, 8, 4) 48
+0.27712173 # (15, 0, 0) 150
+0.27650795 # (15, 1, 0) 96
+0.27589823 # (15, 1, 1) 120
+0.27529253 # (14, 4, 4) 48
+0.27469079 # (15, 2, 0) 120
+0.27409299 # (15, 2, 1) 240
+0.27290900 # (14, 6, 0) 24
+0.27232273 # (15, 2, 2) 144
+0.27174022 # (15, 3, 0) 216
+0.27116143 # (15, 3, 1) 48
+0.27058633 # (14, 6, 2) 72
+0.27001486 # (14, 5, 4) 144
+0.26944701 # (15, 3, 2) 96
+0.26776470 # (15, 4, 0) 144
+0.26721090 # (15, 4, 1) 132
+0.26666051 # (15, 3, 3) 104
+0.26611352 # (12, 10, 0) 72
+0.26556987 # (15, 4, 2) 168
+0.26502955 # (14, 7, 1) 144
+0.26395871 # (14, 6, 4) 96
+0.26342814 # (14, 7, 2) 144
+0.26290076 # (15, 5, 0) 144
+0.26237653 # (15, 5, 1) 168
+0.26133741 # (12, 10, 3) 48
+0.26082246 # (15, 5, 2) 192
+0.25980162 # (16, 0, 0) 6
+0.25929568 # (16, 1, 0) 192
+0.25879268 # (16, 1, 1) 96
+0.25829260 # (15, 5, 3) 96
+0.25779540 # (16, 2, 0) 96
+0.25730107 # (16, 2, 1) 216
+0.25680957 # (15, 6, 1) 72
+0.25583496 # (16, 2, 2) 96
+0.25535179 # (16, 3, 0) 96
+0.25487135 # (16, 3, 1) 240
+0.25439362 # (13, 7, 7) 48
+0.25391856 # (14, 6, 6) 24
+0.25344615 # (16, 3, 2) 264
+0.25297637 # (15, 6, 3) 192
+0.25204460 # (16, 4, 0) 48
+0.25158255 # (16, 4, 1) 96
+0.25112304 # (16, 3, 3) 144
+0.25066604 # (15, 7, 1) 120
+0.25021152 # (16, 4, 2) 96
+0.24975947 # (15, 6, 4) 72
+0.24930986 # (15, 7, 2) 168
+0.24841787 # (12, 10, 6) 48
+0.24797545 # (16, 5, 0) 240
+0.24753539 # (16, 5, 1) 96
+0.24709766 # (15, 7, 3) 72
+0.24622912 # (16, 5, 2) 192
+0.24579827 # (15, 6, 5) 144
+0.24494332 # (16, 4, 4) 36
+0.24451918 # (17, 0, 0) 102
+0.24409723 # (17, 1, 0) 240
+0.24367746 # (17, 1, 1) 96
+0.24325984 # (16, 6, 0) 48
+0.24284437 # (17, 2, 0) 216
+0.24243102 # (17, 2, 1) 168
+0.24161060 # (16, 6, 2) 120
+0.24120351 # (17, 2, 2) 192
+0.24079846 # (17, 3, 0) 72
+0.24039545 # (17, 3, 1) 192
+0.23999446 # (14, 10, 2) 56
+0.23959547 # (16, 6, 3) 96
+0.23919846 # (17, 3, 2) 144
+0.23841032 # (12, 12, 4) 24
+0.23801916 # (17, 4, 0) 192
+0.23762993 # (17, 4, 1) 240
+0.23724259 # (17, 3, 3) 72
+0.23685714 # (16, 6, 4) 96
+0.23647357 # (17, 4, 2) 144
+0.23609185 # (15, 9, 2) 96
+0.23533393 # (14, 10, 4) 48
+0.23495770 # (14, 9, 6) 96
+0.23458326 # (17, 5, 0) 312
+0.23421061 # (17, 5, 1) 144
+0.23347061 # (16, 6, 5) 120
+0.23310323 # (17, 5, 2) 144
+0.23237364 # (16, 8, 0) 24
+0.23201140 # (17, 4, 4) 240
+0.23165086 # (15, 9, 4) 96
+0.23129199 # (17, 5, 3) 96
+0.23093478 # (18, 0, 0) 102
+0.23057922 # (18, 1, 0) 168
+0.23022530 # (18, 1, 1) 264
+0.22952232 # (18, 2, 0) 48
+0.22917324 # (18, 2, 1) 288
+0.22882574 # (17, 5, 4) 96
+0.22847982 # (15, 9, 5) 72
+0.22813546 # (18, 2, 2) 72
+0.22779266 # (18, 3, 0) 120
+0.22745140 # (18, 3, 1) 144
+0.22677345 # (16, 8, 4) 48
+0.22643674 # (18, 3, 2) 96
+0.22610153 # (17, 7, 0) 180
+0.22576780 # (17, 7, 1) 144
+0.22543554 # (18, 4, 0) 48
+0.22510475 # (18, 4, 1) 336
+0.22477541 # (18, 3, 3) 216
+0.22412104 # (18, 4, 2) 120
+0.22379599 # (16, 8, 5) 96
+0.22347235 # (16, 9, 3) 120
+0.22315011 # (17, 7, 3) 120
+0.22250979 # (18, 5, 0) 168
+0.22219170 # (18, 5, 1) 240
+0.22155957 # (12, 12, 8) 24
+0.22124552 # (18, 5, 2) 192
+0.22093281 # (17, 8, 1) 192
+0.22062142 # (15, 11, 3) 96
+0.22031134 # (18, 4, 4) 144
+0.22000256 # (17, 8, 2) 96
+0.21969508 # (18, 5, 3) 72
+0.21908397 # (18, 6, 0) 120
+0.21878032 # (19, 0, 0) 126
+0.21847792 # (19, 1, 0) 216
+0.21817678 # (19, 1, 1) 104
+0.21787688 # (18, 6, 2) 48
+0.21757822 # (19, 2, 0) 240
+0.21728078 # (19, 2, 1) 144
+0.21639572 # (19, 2, 2) 288
+0.21610309 # (19, 3, 0) 144
+0.21581165 # (19, 3, 1) 192
+0.21552139 # (16, 10, 4) 48
+0.21523229 # (18, 7, 0) 120
+0.21494436 # (19, 3, 2) 336
+0.21437193 # (18, 6, 4) 96
+0.21408743 # (19, 4, 0) 192
+0.21380406 # (19, 4, 1) 192
+0.21352181 # (19, 3, 3) 72
+0.21296065 # (19, 4, 2) 240
+0.21268172 # (18, 7, 3) 96
+0.21212714 # (16, 8, 8) 24
+0.21185147 # (18, 6, 5) 96
+0.21157687 # (19, 5, 0) 240
+0.21130334 # (19, 5, 1) 120
+0.21103087 # (18, 8, 0) 48
+0.21075944 # (18, 8, 1) 264
+0.21048907 # (19, 5, 2) 192
+0.20995142 # (18, 8, 2) 108
+0.20968413 # (19, 4, 4) 144
+0.20941787 # (15, 13, 0) 120
+0.20915261 # (19, 5, 3) 192
+0.20888837 # (18, 6, 6) 72
+0.20862512 # (19, 6, 0) 72
+0.20836286 # (19, 6, 1) 240
+0.20784130 # (20, 0, 0) 30
+0.20758198 # (20, 1, 0) 240
+0.20732364 # (20, 1, 1) 192
+0.20706625 # (15, 13, 3) 48
+0.20680982 # (20, 2, 0) 168
+0.20655434 # (20, 2, 1) 216
+0.20629981 # (19, 6, 3) 192
+0.20579355 # (20, 2, 2) 48
+0.20554182 # (20, 3, 0) 192
+0.20529100 # (20, 3, 1) 192
+0.20504111 # (19, 7, 1) 144
+0.20454404 # (20, 3, 2) 240
+0.20429685 # (19, 7, 2) 240
+0.20380516 # (20, 4, 0) 72
+0.20356065 # (20, 4, 1) 144
+0.20331701 # (20, 3, 3) 96
+0.20307424 # (19, 7, 3) 216
+0.20283234 # (20, 4, 2) 96
+0.20259130 # (18, 9, 4) 120
+0.20235112 # (19, 6, 5) 120
+0.20187332 # (18, 10, 0) 72
+0.20163568 # (20, 5, 0) 336
+0.20139888 # (20, 5, 1) 288
+0.20116291 # (15, 11, 9) 48
+0.20092777 # (18, 10, 2) 72
+0.20069345 # (20, 5, 2) 192
+0.20045995 # (18, 9, 5) 144
+0.19999538 # (20, 4, 4) 32
+0.19976431 # (19, 6, 6) 144
+0.19953403 # (20, 5, 3) 288
+0.19930455 # (19, 7, 5) 96
+0.19907586 # (20, 6, 0) 72
+0.19884795 # (20, 6, 1) 240
+0.19862083 # (17, 10, 7) 96
+0.19816890 # (20, 6, 2) 144
+0.19794410 # (21, 0, 0) 270
+0.19772005 # (21, 1, 0) 96
+0.19749676 # (21, 1, 1) 120
+0.19705245 # (21, 2, 0) 96
+0.19683142 # (21, 2, 1) 384
+0.19617275 # (21, 2, 2) 240
+0.19595466 # (21, 3, 0) 252
+0.19573729 # (21, 3, 1) 144
+0.19552065 # (20, 6, 4) 96
+0.19530472 # (20, 7, 2) 144
+0.19508951 # (21, 3, 2) 168
+0.19466121 # (16, 14, 2) 96
+0.19444812 # (21, 4, 0) 96
+0.19423572 # (21, 4, 1) 312
+0.19402402 # (21, 3, 3) 192
+0.19381301 # (18, 10, 6) 48
+0.19360269 # (21, 4, 2) 360
+0.19339305 # (19, 10, 1) 96
+0.19297580 # (20, 8, 0) 72
+0.19276819 # (20, 8, 1) 192
+0.19256125 # (21, 5, 0) 96
+0.19235497 # (21, 5, 1) 168
+0.19214935 # (20, 8, 2) 120
+0.19194439 # (18, 12, 1) 192
+0.19174009 # (21, 5, 2) 240
+0.19133343 # (20, 6, 6) 72
+0.19113106 # (21, 4, 4) 144
+0.19092934 # (20, 7, 5) 240
+0.19072826 # (21, 5, 3) 120
+0.19032799 # (21, 6, 0) 216
+0.19012880 # (21, 6, 1) 96
+0.18973228 # (20, 8, 4) 48
+0.18953495 # (21, 6, 2) 192
+0.18933824 # (21, 5, 4) 240
+0.18914213 # (19, 11, 1) 96
+0.18894664 # (22, 0, 0) 78
+0.18875175 # (22, 1, 0) 240
+0.18855746 # (22, 1, 1) 312
+0.18817067 # (22, 2, 0) 120
+0.18797817 # (22, 2, 1) 240
+0.18778626 # (21, 7, 0) 168
+0.18759493 # (21, 7, 1) 216
+0.18740419 # (22, 2, 2) 48
+0.18721403 # (22, 3, 0) 144
+0.18702444 # (22, 3, 1) 336
+0.18645913 # (22, 3, 2) 288
+0.18627183 # (20, 7, 7) 96
+0.18608509 # (21, 7, 3) 72
+0.18589891 # (22, 4, 0) 144
+0.18571329 # (22, 4, 1) 192
+0.18552822 # (22, 3, 3) 168
+0.18515975 # (22, 4, 2) 144
+0.18497633 # (21, 8, 0) 96
+0.18479346 # (21, 8, 1) 336
+0.18461112 # (19, 11, 5) 104
+0.18424807 # (22, 5, 0) 360
+0.18406735 # (22, 5, 1) 192
+0.18370749 # (16, 16, 0) 12
+0.18352835 # (22, 5, 2) 192
+0.18334973 # (21, 8, 3) 192
+0.18317164 # (21, 7, 5) 144
+0.18299406 # (22, 4, 4) 144
+0.18281700 # (20, 9, 6) 144
+0.18264045 # (22, 5, 3) 192
+0.18228888 # (22, 6, 0) 48
+0.18211385 # (22, 6, 1) 384
+0.18193933 # (21, 9, 0) 120
+0.18176531 # (21, 9, 1) 120
+0.18159179 # (22, 6, 2) 120
+0.18141876 # (22, 5, 4) 240
+0.18124623 # (21, 9, 2) 144
+0.18090263 # (20, 8, 8) 48
+0.18073157 # (23, 0, 0) 150
+0.18056098 # (23, 1, 0) 336
+0.18039088 # (23, 1, 1) 216
+0.18022126 # (18, 12, 8) 48
+0.18005212 # (23, 2, 0) 144
+0.17988345 # (23, 2, 1) 240
+0.17954754 # (22, 6, 4) 168
+0.17938028 # (23, 2, 2) 144
+0.17921349 # (23, 3, 0) 120
+0.17904717 # (23, 3, 1) 216
+0.17871591 # (21, 10, 0) 120
+0.17855097 # (23, 3, 2) 288
+0.17822244 # (20, 12, 0) 48
+0.17805886 # (23, 4, 0) 384
+0.17789573 # (23, 4, 1) 288
+0.17773305 # (23, 3, 3) 72
+0.17757081 # (22, 8, 0) 96
+0.17740901 # (23, 4, 2) 360
+0.17724766 # (21, 10, 3) 168
+0.17692626 # (22, 8, 2) 96
+0.17676622 # (20, 12, 3) 96
+0.17660661 # (23, 5, 0) 264
+0.17644744 # (23, 5, 1) 96
+0.17628869 # (22, 6, 6) 72
+0.17613037 # (22, 8, 3) 216
+0.17597248 # (23, 5, 2) 288
+0.17565796 # (20, 12, 4) 48
+0.17550133 # (23, 4, 4) 192
+0.17534512 # (21, 11, 0) 96
+0.17518933 # (23, 5, 3) 216
+0.17503395 # (22, 8, 4) 96
+0.17487898 # (23, 6, 0) 144
+0.17472443 # (23, 6, 1) 360
+0.17441654 # (18, 12, 10) 48
+0.17426321 # (23, 6, 2) 384
+0.17411028 # (23, 5, 4) 192
+0.17395775 # (21, 11, 3) 120
+0.17365390 # (22, 8, 5) 192
+0.17350256 # (23, 6, 3) 192
+0.17320108 # (24, 0, 0) 30
+0.17305093 # (24, 1, 0) 96
+0.17290117 # (24, 1, 1) 204
+0.17275179 # (23, 7, 1) 192
+0.17260280 # (24, 2, 0) 96
+0.17245420 # (24, 2, 1) 336
+0.17230598 # (23, 7, 2) 192
+0.17201068 # (24, 2, 2) 192
+0.17186360 # (24, 3, 0) 288
+0.17171690 # (24, 3, 1) 216
+0.17157057 # (23, 7, 3) 168
+0.17142462 # (22, 10, 2) 56
+0.17127903 # (24, 3, 2) 192
+0.17113382 # (23, 6, 5) 240
+0.17084450 # (24, 4, 0) 24
+0.17070038 # (24, 4, 1) 288
+0.17055664 # (24, 3, 3) 384
+0.17041325 # (19, 15, 3) 96
+0.17027023 # (24, 4, 2) 168
+0.17012756 # (23, 8, 2) 144
+0.16998526 # (21, 11, 6) 96
+0.16970171 # (22, 10, 4) 120
+0.16956047 # (24, 5, 0) 240
+0.16941958 # (24, 5, 1) 288
+0.16927904 # (23, 7, 5) 120
+0.16899901 # (24, 5, 2) 312
+0.16885951 # (22, 11, 1) 144
+0.16858156 # (24, 4, 4) 72
+0.16844309 # (23, 8, 4) 192
+0.16830497 # (24, 5, 3) 144
+0.16816718 # (23, 9, 1) 240
+0.16802973 # (24, 6, 0) 144
+0.16789262 # (24, 6, 1) 120
+0.16775584 # (23, 9, 2) 408
+0.16748329 # (24, 6, 2) 96
+0.16734751 # (24, 5, 4) 144
+0.16721206 # (23, 8, 5) 144
+0.16707694 # (23, 9, 3) 120
+0.16694215 # (22, 10, 6) 96
+0.16680768 # (24, 6, 3) 384
+0.16667354 # (21, 10, 9) 144
+0.16627304 # (25, 0, 0) 150
+0.16614018 # (25, 1, 0) 432
+0.16600764 # (25, 1, 1) 96
+0.16587542 # (24, 6, 4) 72
+0.16574351 # (25, 2, 0) 432
+0.16561191 # (25, 2, 1) 240
+0.16534966 # (22, 12, 2) 96
+0.16521900 # (25, 2, 2) 240
+0.16508865 # (25, 3, 0) 168
+0.16495861 # (25, 3, 1) 240
+0.16469944 # (24, 6, 5) 168
+0.16457032 # (25, 3, 2) 240
+0.16431297 # (24, 8, 0) 24
+0.16418476 # (25, 4, 0) 336
+0.16405684 # (25, 4, 1) 192
+0.16392922 # (25, 3, 3) 72
+0.16380189 # (24, 8, 2) 192
+0.16367486 # (25, 4, 2) 192
+0.16354813 # (23, 9, 6) 192
+0.16329555 # (24, 6, 6) 108
+0.16316969 # (24, 8, 3) 240
+0.16304413 # (25, 5, 0) 360
+0.16291886 # (25, 5, 1) 192
+0.16279387 # (18, 18, 2) 24
+0.16266917 # (22, 13, 0) 168
+0.16254476 # (25, 5, 2) 336
+0.16229679 # (24, 8, 4) 96
+0.16217323 # (25, 4, 4) 240
+0.16204995 # (24, 9, 1) 96
+0.16192695 # (25, 5, 3) 264
+0.16180423 # (20, 16, 2) 96
+0.16168179 # (25, 6, 0) 216
+0.16155963 # (25, 6, 1) 264
+0.16131613 # (22, 12, 6) 120
+0.16119480 # (25, 6, 2) 288
+0.16107374 # (25, 5, 4) 360
+0.16095295 # (21, 15, 1) 96
+0.16071218 # (22, 13, 4) 144
+0.16059220 # (25, 6, 3) 144
+0.16035304 # (20, 16, 4) 48
+0.16023387 # (24, 9, 4) 144
+0.16011495 # (25, 7, 0) 288
+0.15999631 # (25, 7, 1) 224
+0.15987792 # (26, 0, 0) 78
+0.15975980 # (26, 1, 0) 360
+0.15964194 # (26, 1, 1) 240
+0.15940700 # (26, 2, 0) 144
+0.15928992 # (26, 2, 1) 240
+0.15917309 # (24, 9, 5) 144
+0.15905653 # (25, 7, 3) 120
+0.15894021 # (26, 2, 2) 120
+0.15882416 # (26, 3, 0) 144
+0.15870835 # (26, 3, 1) 384
+0.15847751 # (20, 12, 12) 24
+0.15836246 # (26, 3, 2) 480
+0.15824766 # (25, 8, 1) 192
+0.15813311 # (23, 9, 9) 120
+0.15801881 # (26, 4, 0) 168
+0.15790476 # (26, 4, 1) 288
+0.15779096 # (26, 3, 3) 120
+0.15756408 # (26, 4, 2) 144
+0.15745101 # (25, 6, 6) 96
+0.15733818 # (25, 8, 3) 312
+0.15722560 # (25, 7, 5) 240
+0.15700115 # (26, 5, 0) 408
+0.15688929 # (26, 5, 1) 192
+0.15666627 # (24, 8, 8) 24
+0.15655512 # (26, 5, 2) 288
+0.15644421 # (25, 9, 0) 288
+0.15633353 # (25, 9, 1) 144
+0.15622309 # (26, 4, 4) 48
+0.15611288 # (23, 12, 6) 120
+0.15600290 # (26, 5, 3) 384
+0.15578364 # (26, 6, 0) 96
+0.15567436 # (26, 6, 1) 288
+0.15556530 # (25, 8, 5) 288
+0.15545648 # (25, 9, 3) 96
+0.15534788 # (26, 6, 2) 120
+0.15523951 # (26, 5, 4) 192
+0.15513137 # (22, 15, 3) 144
+0.15491576 # (24, 12, 0) 72
+0.15480829 # (26, 6, 3) 192
+0.15470104 # (25, 9, 4) 228
+0.15459402 # (25, 7, 7) 96
+0.15448722 # (24, 12, 2) 120
+0.15438064 # (26, 7, 0) 360
+0.15427428 # (26, 7, 1) 264
+0.15406222 # (26, 6, 4) 144
+0.15395652 # (27, 0, 0) 318
+0.15385103 # (27, 1, 0) 144
+0.15374576 # (27, 1, 1) 288
+0.15353587 # (27, 2, 0) 168
+0.15343125 # (27, 2, 1) 480
+0.15322264 # (24, 12, 4) 48
+0.15311865 # (27, 2, 2) 240
+0.15301488 # (27, 3, 0) 240
+0.15291132 # (27, 3, 1) 120
+0.15280796 # (26, 8, 0) 192
+0.15270482 # (26, 8, 1) 288
+0.15260188 # (27, 3, 2) 96
+0.15239664 # (26, 8, 2) 144
+0.15229432 # (27, 4, 0) 192
+0.15219221 # (27, 4, 1) 312
+0.15209031 # (27, 3, 3) 216
+0.15198861 # (26, 6, 6) 48
+0.15188712 # (27, 4, 2) 384
+0.15178582 # (26, 7, 5) 288
+0.15148316 # (25, 8, 8) 144
+0.15138267 # (27, 5, 0) 240
+0.15128239 # (27, 5, 1) 288
+0.15118230 # (26, 8, 4) 192
+0.15108241 # (26, 9, 0) 120
+0.15098272 # (27, 5, 2) 264
+0.15078393 # (20, 18, 6) 48
+0.15068482 # (27, 4, 4) 480
+0.15058592 # (25, 11, 4) 144
+0.15048721 # (27, 5, 3) 96
+0.15029036 # (27, 6, 0) 240
+0.15019223 # (27, 6, 1) 288
+0.14999654 # (16, 16, 16) 8
+0.14989898 # (27, 6, 2) 240
+0.14980161 # (27, 5, 4) 384
+0.14970443 # (25, 11, 5) 144
+0.14960744 # (24, 14, 0) 48
+0.14951064 # (26, 9, 4) 312
+0.14941403 # (27, 6, 3) 360
+0.14922136 # (26, 10, 0) 240
+0.14912530 # (26, 10, 1) 192
+0.14902943 # (27, 7, 0) 168
+0.14893375 # (27, 7, 1) 240
+0.14883825 # (26, 10, 2) 96
+0.14874293 # (27, 6, 4) 240
+0.14864779 # (27, 7, 2) 288
+0.14845807 # (28, 0, 0) 54
+0.14836348 # (28, 1, 0) 192
+0.14826907 # (28, 1, 1) 192
+0.14817484 # (27, 7, 3) 120
+0.14808079 # (28, 2, 0) 120
+0.14798692 # (28, 2, 1) 384
+0.14789323 # (27, 6, 5) 192
+0.14770638 # (28, 2, 2) 120
+0.14761322 # (28, 3, 0) 96
+0.14752023 # (28, 3, 1) 504
+0.14742743 # (25, 13, 1) 96
+0.14724233 # (28, 3, 2) 360
+0.14715005 # (26, 11, 1) 192
+0.14696599 # (28, 4, 0) 84
+0.14687422 # (28, 4, 1) 432
+0.14678263 # (28, 3, 3) 144
+0.14669120 # (27, 7, 5) 240
+0.14659995 # (28, 4, 2) 144
+0.14650887 # (25, 12, 6) 192
+0.14641795 # (26, 11, 3) 336
+0.14623663 # (24, 14, 6) 72
+0.14614622 # (28, 5, 0) 384
+0.14605598 # (28, 5, 1) 408
+0.14596590 # (27, 9, 1) 168
+0.14587599 # (26, 10, 6) 96
+0.14578625 # (28, 5, 2) 144
+0.14569668 # (27, 9, 2) 144
+0.14551802 # (28, 4, 4) 48
+0.14542893 # (24, 15, 4) 144
+0.14534001 # (28, 5, 3) 336
+0.14525126 # (27, 9, 3) 240
+0.14516266 # (28, 6, 0) 96
+0.14507423 # (28, 6, 1) 360
+0.14498596 # (26, 11, 5) 240
+0.14480990 # (28, 6, 2) 240
+0.14472211 # (28, 5, 4) 336
+0.14463447 # (27, 9, 4) 144
+0.14454700 # (27, 7, 7) 168
+0.14437253 # (28, 6, 3) 264
+0.14428554 # (27, 10, 1) 240
+0.14411201 # (24, 16, 0) 24
+0.14402548 # (28, 7, 0) 336
+0.14393911 # (28, 7, 1) 192
+0.14385290 # (27, 9, 5) 144
+0.14376683 # (28, 6, 4) 240
+0.14368093 # (28, 7, 2) 192
+0.14359517 # (27, 10, 3) 168
+0.14342412 # (26, 10, 8) 96
+0.14333883 # (29, 0, 0) 174
+0.14325368 # (29, 1, 0) 312
+0.14316869 # (29, 1, 1) 144
+0.14308385 # (22, 18, 6) 72
+0.14299916 # (29, 2, 0) 360
+0.14291462 # (29, 2, 1) 480
+0.14274599 # (28, 8, 0) 72
+0.14266190 # (29, 2, 2) 336
+0.14257796 # (29, 3, 0) 240
+0.14249416 # (29, 3, 1) 240
+0.14241051 # (28, 8, 2) 96
+0.14232701 # (24, 14, 9) 120
+0.14224366 # (29, 3, 2) 528
+0.14207739 # (28, 6, 6) 72
+0.14199447 # (29, 4, 0) 384
+0.14191170 # (29, 4, 1) 192
+0.14182907 # (29, 3, 3) 168
+0.14166425 # (29, 4, 2) 288
+0.14158206 # (23, 18, 3) 96
+0.14141809 # (28, 8, 4) 96
+0.14133632 # (28, 9, 0) 192
+0.14125470 # (29, 5, 0) 528
+0.14117321 # (29, 5, 1) 152
+0.14109187 # (24, 16, 6) 96
+0.14101066 # (28, 9, 2) 384
+0.14092960 # (29, 5, 2) 192
+0.14076789 # (26, 14, 0) 120
+0.14068724 # (29, 4, 4) 240
+0.14060674 # (28, 9, 3) 240
+0.14052637 # (29, 5, 3) 288
+0.14044614 # (26, 14, 2) 96
+0.14036604 # (29, 6, 0) 120
+0.14028608 # (29, 6, 1) 240
+0.14004703 # (29, 6, 2) 480
+0.13996761 # (29, 5, 4) 324
+0.13988833 # (21, 21, 1) 72
+0.13980919 # (28, 10, 0) 192
+0.13973018 # (28, 10, 1) 288
+0.13965130 # (29, 6, 3) 216
+0.13949395 # (28, 10, 2) 144
+0.13941547 # (27, 12, 4) 192
+0.13933712 # (29, 7, 0) 288
+0.13925891 # (29, 7, 1) 216
+0.13910288 # (29, 6, 4) 336
+0.13902506 # (29, 7, 2) 336
+0.13886981 # (24, 16, 8) 48
+0.13879238 # (28, 8, 7) 192
+0.13871508 # (27, 13, 0) 144
+0.13863791 # (29, 7, 3) 336
+0.13856087 # (30, 0, 0) 150
+0.13848395 # (30, 1, 0) 288
+0.13840717 # (30, 1, 1) 336
+0.13825398 # (30, 2, 0) 96
+0.13817757 # (30, 2, 1) 288
+0.13810129 # (29, 8, 1) 336
+0.13802514 # (27, 13, 3) 72
+0.13794912 # (30, 2, 2) 120
+0.13787322 # (30, 3, 0) 504
+0.13779744 # (30, 3, 1) 192
+0.13764626 # (28, 8, 8) 48
+0.13757086 # (30, 3, 2) 144
+0.13749558 # (29, 8, 3) 432
+0.13742043 # (29, 7, 5) 192
+0.13734540 # (30, 4, 0) 120
+0.13727049 # (30, 4, 1) 240
+0.13719570 # (30, 3, 3) 192
+0.13704649 # (30, 4, 2) 240
+0.13697207 # (29, 8, 4) 240
+0.13689777 # (29, 9, 0) 216
+0.13682359 # (29, 9, 1) 240
+0.13667560 # (30, 5, 0) 168
+0.13660178 # (30, 5, 1) 480
+0.13645450 # (28, 12, 0) 24
+0.13638104 # (30, 5, 2) 432
+0.13630769 # (29, 8, 5) 288
+0.13623447 # (29, 9, 3) 168
+0.13616136 # (30, 4, 4) 144
+0.13608837 # (28, 10, 7) 192
+0.13601550 # (30, 5, 3) 312
+0.13587011 # (30, 6, 0) 216
+0.13579759 # (30, 6, 1) 240
+0.13572518 # (29, 9, 4) 192
+0.13565289 # (29, 7, 7) 192
+0.13558071 # (30, 6, 2) 48
+0.13550865 # (30, 5, 4) 552
+0.13543671 # (29, 10, 1) 144
+0.13529316 # (28, 12, 4) 72
+0.13522156 # (30, 6, 3) 384
+0.13515007 # (28, 9, 9) 192
+0.13507870 # (29, 9, 5) 120
+0.13500743 # (28, 10, 8) 144
+0.13493628 # (30, 7, 0) 144
+0.13486524 # (30, 7, 1) 504
+0.13472350 # (30, 6, 4) 96
+0.13465280 # (30, 7, 2) 384
+0.13458221 # (29, 8, 7) 360
+0.13451173 # (27, 15, 1) 96
+0.13437110 # (29, 10, 4) 192
+0.13430095 # (30, 7, 3) 192
+0.13409116 # (31, 0, 0) 198
+0.13402145 # (31, 1, 0) 336
+0.13395185 # (31, 1, 1) 216
+0.13388235 # (30, 8, 0) 144
+0.13381296 # (31, 2, 0) 528
+0.13374368 # (31, 2, 1) 288
+0.13360545 # (30, 8, 2) 132
+0.13353649 # (31, 2, 2) 288
+0.13346764 # (31, 3, 0) 144
+0.13339889 # (31, 3, 1) 360
+0.13333026 # (30, 6, 6) 104
+0.13326172 # (30, 8, 3) 144
+0.13319330 # (31, 3, 2) 432
+0.13305676 # (24, 20, 0) 72
+0.13298865 # (31, 4, 0) 240
+0.13292064 # (31, 4, 1) 288
+0.13285274 # (31, 3, 3) 192
+0.13278494 # (30, 8, 4) 168
+0.13271724 # (31, 4, 2) 360
+0.13264965 # (30, 9, 1) 120
+0.13251477 # (28, 14, 2) 144
+0.13244749 # (30, 9, 2) 288
+0.13238031 # (31, 5, 0) 528
+0.13231323 # (31, 5, 1) 192
+0.13217938 # (30, 8, 5) 432
+0.13211260 # (31, 5, 2) 432
+0.13197936 # (28, 12, 8) 96
+0.13191289 # (31, 4, 4) 144
+0.13184651 # (29, 12, 3) 192
+0.13178024 # (31, 5, 3) 192
+0.13171407 # (28, 14, 4) 144
+0.13164800 # (31, 6, 0) 168
+0.13158203 # (31, 6, 1) 312
+0.13145038 # (30, 10, 0) 144
+0.13138470 # (31, 6, 2) 480
+0.13131913 # (31, 5, 4) 192
+0.13125365 # (29, 9, 9) 96
+0.13118827 # (30, 10, 2) 168
+0.13112298 # (29, 10, 8) 192
+0.13105779 # (31, 6, 3) 240
+0.13086282 # (30, 10, 3) 240
+0.13079802 # (31, 7, 0) 336
+0.13073331 # (31, 7, 1) 288
+0.13066871 # (24, 20, 6) 48
+0.13060419 # (31, 6, 4) 312
+0.13053978 # (31, 7, 2) 360
+0.13041123 # (30, 10, 4) 192
+0.13034710 # (30, 9, 6) 288
+0.13028306 # (28, 15, 3) 216
+0.13021912 # (31, 7, 3) 312
+0.13009152 # (30, 11, 0) 264
+0.13002785 # (31, 6, 5) 384
+0.12990081 # (32, 0, 0) 6
+0.12983743 # (32, 1, 0) 480
+0.12977414 # (32, 1, 1) 384
+0.12971095 # (27, 17, 3) 96
+0.12964784 # (32, 2, 0) 192
+0.12958483 # (32, 2, 1) 384
+0.12952191 # (30, 11, 3) 144
+0.12939634 # (32, 2, 2) 96
+0.12933369 # (32, 3, 0) 144
+0.12927114 # (32, 3, 1) 528
+0.12920867 # (31, 7, 5) 240
+0.12914630 # (30, 10, 6) 96
+0.12908402 # (32, 3, 2) 240
+0.12902182 # (29, 14, 1) 144
+0.12889770 # (32, 4, 0) 96
+0.12883578 # (32, 4, 1) 432
+0.12877394 # (32, 3, 3) 144
+0.12871219 # (31, 9, 1) 192
+0.12865053 # (32, 4, 2) 216
+0.12858896 # (30, 12, 1) 192
+0.12852748 # (31, 9, 2) 504
+0.12840478 # (30, 12, 2) 72
+0.12834357 # (32, 5, 0) 528
+0.12828244 # (32, 5, 1) 336
+0.12822139 # (31, 9, 3) 120
+0.12809957 # (32, 5, 2) 408
+0.12803878 # (27, 18, 1) 192
+0.12791748 # (32, 4, 4) 96
+0.12785695 # (30, 11, 6) 192
+0.12779652 # (32, 5, 3) 300
+0.12773616 # (31, 7, 7) 144
+0.12767590 # (32, 6, 0) 96
+0.12761571 # (32, 6, 1) 312
+0.12755562 # (31, 10, 1) 360
+0.12743568 # (32, 6, 2) 240
+0.12737583 # (32, 5, 4) 192
+0.12731608 # (29, 15, 0) 240
+0.12725640 # (31, 9, 5) 288
+0.12719681 # (26, 14, 14) 48
+0.12713730 # (32, 6, 3) 360
+0.12707788 # (31, 10, 3) 432
+0.12695928 # (28, 12, 12) 24
+0.12690011 # (32, 7, 0) 288
+0.12684101 # (32, 7, 1) 384
+0.12678200 # (29, 15, 3) 168
+0.12672308 # (32, 6, 4) 264
+0.12666423 # (32, 7, 2) 288
+0.12660547 # (31, 9, 6) 216
+0.12648819 # (30, 12, 6) 192
+0.12642967 # (30, 10, 9) 192
+0.12637123 # (32, 7, 3) 264
+0.12631287 # (31, 11, 1) 152
+0.12619640 # (32, 6, 5) 384
+0.12613829 # (31, 11, 2) 336
+0.12602230 # (32, 8, 0) 48
+0.12596442 # (33, 0, 0) 390
+0.12590663 # (33, 1, 0) 144
+0.12584891 # (33, 1, 1) 408
+0.12579128 # (32, 8, 2) 96
+0.12573372 # (33, 2, 0) 120
+0.12567624 # (33, 2, 1) 312
+0.12556152 # (32, 6, 6) 144
+0.12550428 # (33, 2, 2) 432
+0.12544711 # (33, 3, 0) 360
+0.12539003 # (33, 3, 1) 144
+0.12533302 # (30, 14, 2) 120
+0.12527609 # (29, 16, 2) 336
+0.12521924 # (33, 3, 2) 240
+0.12510576 # (32, 8, 4) 96
+0.12504914 # (33, 4, 0) 192
+0.12499259 # (33, 4, 1) 576
+0.12493613 # (33, 3, 3) 192
+0.12487973 # (30, 12, 8) 72
+0.12482342 # (33, 4, 2) 600
+0.12476718 # (31, 10, 7) 192
+0.12465493 # (30, 14, 4) 168
+0.12459892 # (32, 8, 5) 192
+0.12454298 # (33, 5, 0) 264
+0.12448712 # (33, 5, 1) 240
+0.12437562 # (28, 18, 3) 168
+0.12431998 # (33, 5, 2) 432
+0.12420893 # (24, 20, 12) 48
+0.12415352 # (33, 4, 4) 528
+0.12409818 # (32, 7, 7) 192
+0.12404292 # (33, 5, 3) 120
+0.12398772 # (32, 10, 0) 240
+0.12393261 # (33, 6, 0) 432
+0.12387756 # (33, 6, 1) 264
+0.12376769 # (32, 10, 2) 96
+0.12371287 # (33, 6, 2) 192
+0.12365812 # (33, 5, 4) 528
+0.12360344 # (31, 13, 1) 192
+0.12354883 # (30, 14, 6) 72
+0.12349429 # (32, 10, 3) 336
+0.12343983 # (33, 6, 3) 432
+0.12327687 # (32, 8, 7) 240
+0.12322270 # (33, 7, 0) 144
+0.12316859 # (33, 7, 1) 384
+0.12311456 # (32, 10, 4) 192
+0.12306060 # (33, 6, 4) 288
+0.12300671 # (33, 7, 2) 216
+0.12289914 # (30, 12, 10) 144
+0.12284546 # (32, 11, 0) 288
+0.12279185 # (32, 11, 1) 384
+0.12273831 # (33, 7, 3) 144
+0.12263144 # (32, 11, 2) 192
+0.12257811 # (33, 6, 5) 240
+0.12247166 # (32, 8, 8) 36
+0.12241854 # (33, 8, 0) 192
+0.12236549 # (33, 8, 1) 672
+0.12231250 # (31, 13, 5) 192
+0.12225959 # (34, 0, 0) 102
+0.12220674 # (34, 1, 0) 336
+0.12215396 # (34, 1, 1) 288
+0.12204861 # (34, 2, 0) 240
+0.12199604 # (34, 2, 1) 576
+0.12194354 # (33, 8, 3) 144
+0.12189110 # (33, 7, 5) 168
+0.12183873 # (34, 2, 2) 96
+0.12178643 # (34, 3, 0) 240
+0.12173419 # (34, 3, 1) 432
+0.12162992 # (32, 12, 0) 48
+0.12157789 # (34, 3, 2) 576
+0.12152592 # (33, 9, 0) 240
+0.12147402 # (33, 9, 1) 168
+0.12142218 # (34, 4, 0) 216
+0.12137042 # (34, 4, 1) 288
+0.12131871 # (34, 3, 3) 360
+0.12121551 # (34, 4, 2) 168
+0.12116400 # (32, 12, 3) 144
+0.12111256 # (33, 8, 5) 192
+0.12106119 # (33, 9, 3) 360
+0.12095864 # (34, 5, 0) 552
+0.12090746 # (34, 5, 1) 240
+0.12080530 # (32, 12, 4) 120
+0.12075432 # (34, 5, 2) 192
+0.12070340 # (33, 9, 4) 288
+0.12065254 # (33, 7, 7) 216
+0.12060175 # (34, 4, 4) 192
+0.12055103 # (33, 10, 0) 240
+0.12050037 # (34, 5, 3) 480
+0.12039923 # (34, 6, 0) 72
+0.12034876 # (34, 6, 1) 432
+0.12029835 # (32, 13, 1) 336
+0.12024801 # (33, 9, 5) 192
+0.12019773 # (34, 6, 2) 192
+0.12014751 # (34, 5, 4) 240
+0.12009735 # (33, 10, 3) 144
+0.11999723 # (28, 20, 4) 56
+0.11994726 # (34, 6, 3) 192
+0.11989736 # (33, 8, 7) 288
+0.11984751 # (31, 11, 11) 144
+0.11979773 # (32, 12, 6) 96
+0.11974801 # (34, 7, 0) 480
+0.11969836 # (34, 7, 1) 504
+0.11959923 # (34, 6, 4) 144
+0.11954976 # (34, 7, 2) 480
+0.11950035 # (33, 11, 0) 264
+0.11945100 # (33, 11, 1) 336
+0.11935248 # (30, 13, 12) 120
+0.11930331 # (34, 7, 3) 480
+0.11920516 # (24, 24, 8) 24
+0.11915618 # (34, 6, 5) 384
+0.11910725 # (32, 13, 5) 288
+0.11905839 # (33, 11, 3) 144
+0.11900958 # (34, 8, 0) 192
+0.11896084 # (34, 8, 1) 384
+0.11891215 # (31, 15, 6) 144
+0.11881496 # (34, 8, 2) 240
+0.11876646 # (35, 0, 0) 270
+0.11871801 # (35, 1, 0) 504
+0.11866962 # (35, 1, 1) 96
+0.11862130 # (34, 6, 6) 72
+0.11857303 # (35, 2, 0) 456
+0.11852482 # (35, 2, 1) 288
+0.11842857 # (32, 12, 8) 96
+0.11838054 # (35, 2, 2) 288
+0.11833256 # (35, 3, 0) 288
+0.11828464 # (35, 3, 1) 288
+0.11823678 # (34, 8, 4) 144
+0.11818898 # (34, 9, 0) 168
+0.11814124 # (35, 3, 2) 504
+0.11804593 # (30, 18, 4) 96
+0.11799836 # (35, 4, 0) 384
+0.11795084 # (35, 4, 1) 384
+0.11790339 # (35, 3, 3) 96
+0.11780865 # (35, 4, 2) 384
+0.11776136 # (34, 9, 3) 192
+0.11766697 # (28, 20, 8) 48
+0.11761985 # (33, 12, 4) 384
+0.11757279 # (35, 5, 0) 444
+0.11752579 # (35, 5, 1) 360
+0.11747885 # (28, 18, 12) 96
+0.11743196 # (34, 9, 4) 384
+0.11738513 # (35, 5, 2) 288
+0.11729163 # (34, 10, 0) 312
+0.11724497 # (35, 4, 4) 240
+0.11719836 # (33, 13, 0) 144
+0.11715180 # (35, 5, 3) 360
+0.11710531 # (34, 10, 2) 144
+0.11705886 # (35, 6, 0) 240
+0.11701248 # (35, 6, 1) 432
+0.11687364 # (35, 6, 2) 480
+0.11682748 # (35, 5, 4) 384
+0.11678136 # (33, 13, 3) 144
+0.11673530 # (32, 12, 10) 120
+0.11668930 # (34, 8, 7) 384
+0.11664335 # (35, 6, 3) 240
+0.11655161 # (34, 10, 4) 144
+0.11650583 # (34, 9, 6) 240
+0.11646009 # (35, 7, 0) 504
+0.11641441 # (35, 7, 1) 240
+0.11632322 # (35, 6, 4) 408
+0.11627770 # (35, 7, 2) 240
+0.11618682 # (32, 16, 0) 24
+0.11614146 # (34, 11, 2) 288
+0.11609615 # (33, 12, 7) 144
+0.11605090 # (35, 7, 3) 264
+0.11600570 # (34, 8, 8) 240
+0.11596055 # (33, 14, 0) 144
+0.11591546 # (35, 6, 5) 696
+0.11582543 # (30, 18, 8) 96
+0.11578049 # (35, 8, 0) 432
+0.11573561 # (35, 8, 1) 192
+0.11569077 # (33, 11, 9) 216
+0.11564599 # (34, 10, 6) 96
+0.11560126 # (35, 8, 2) 288
+0.11555659 # (33, 14, 3) 336
+0.11546739 # (36, 0, 0) 102
+0.11542287 # (36, 1, 0) 144
+0.11537840 # (36, 1, 1) 384
+0.11533398 # (35, 7, 5) 192
+0.11528961 # (36, 2, 0) 168
+0.11524529 # (36, 2, 1) 600
+0.11520103 # (34, 11, 5) 192
+0.11511265 # (36, 2, 2) 264
+0.11506854 # (36, 3, 0) 480
+0.11502447 # (36, 3, 1) 216
+0.11498046 # (35, 9, 1) 264
+0.11489259 # (36, 3, 2) 288
+0.11484873 # (35, 9, 2) 432
+0.11476116 # (36, 4, 0) 48
+0.11471745 # (36, 4, 1) 288
+0.11467379 # (36, 3, 3) 576
+0.11463018 # (35, 9, 3) 144
+0.11458662 # (36, 4, 2) 288
+0.11454311 # (32, 17, 2) 240
+0.11449965 # (34, 9, 9) 120
+0.11441287 # (34, 10, 8) 96
+0.11436956 # (36, 5, 0) 288
+0.11432629 # (36, 5, 1) 504
+0.11428308 # (35, 7, 7) 224
+0.11423991 # (30, 18, 10) 72
+0.11419679 # (36, 5, 2) 504
+0.11415372 # (35, 10, 1) 480
+0.11406773 # (36, 4, 4) 72
+0.11402481 # (35, 10, 2) 432
+0.11398193 # (36, 5, 3) 288
+0.11393911 # (35, 9, 5) 288
+0.11389633 # (36, 6, 0) 120
+0.11385360 # (36, 6, 1) 240
+0.11381092 # (35, 10, 3) 384
+0.11372570 # (36, 6, 2) 144
+0.11368316 # (36, 5, 4) 288
+0.11364067 # (35, 8, 7) 240
+0.11359823 # (33, 15, 5) 192
+0.11351348 # (36, 6, 3) 504
+0.11347118 # (35, 9, 6) 240
+0.11338672 # (32, 16, 8) 48
+0.11334457 # (36, 7, 0) 192
+0.11330245 # (36, 7, 1) 336
+0.11326039 # (35, 11, 1) 144
+0.11321837 # (36, 6, 4) 96
+0.11317640 # (36, 7, 2) 672
+0.11313447 # (35, 11, 2) 480
+0.11305076 # (34, 14, 0) 180
+0.11300898 # (35, 8, 8) 192
+0.11296724 # (36, 7, 3) 264
+0.11292555 # (35, 11, 3) 288
+0.11288390 # (34, 14, 2) 144
+0.11284230 # (36, 6, 5) 192
+0.11280074 # (34, 11, 9) 288
+0.11271777 # (36, 8, 0) 48
+0.11267635 # (36, 8, 1) 720
+0.11263498 # (35, 11, 4) 288
+0.11259366 # (33, 15, 7) 144
+0.11255237 # (36, 8, 2) 336
+0.11251114 # (31, 20, 2) 192
+0.11246995 # (33, 14, 9) 216
+0.11238770 # (36, 6, 6) 216
+0.11234665 # (37, 0, 0) 222
+0.11230564 # (37, 1, 0) 528
+0.11226467 # (37, 1, 1) 288
+0.11218288 # (37, 2, 0) 216
+0.11214205 # (37, 2, 1) 336
+0.11206052 # (36, 8, 4) 120
+0.11201982 # (37, 2, 2) 432
+0.11197917 # (37, 3, 0) 240
+0.11193856 # (37, 3, 1) 384
+0.11189799 # (32, 16, 10) 96
+0.11185747 # (36, 9, 2) 312
+0.11181700 # (37, 3, 2) 456
+0.11173618 # (32, 18, 6) 120
+0.11169583 # (37, 4, 0) 576
+0.11165553 # (37, 4, 1) 480
+0.11161527 # (37, 3, 3) 96
+0.11157506 # (34, 14, 6) 120
+0.11153488 # (37, 4, 2) 336
+0.11149476 # (34, 15, 3) 240
+0.11137463 # (36, 9, 4) 192
+0.11133468 # (37, 5, 0) 576
+0.11129477 # (37, 5, 1) 288
+0.11125490 # (36, 10, 0) 168
+0.11121507 # (36, 10, 1) 288
+0.11117529 # (37, 5, 2) 240
+0.11109585 # (36, 10, 2) 240
+0.11105619 # (37, 4, 4) 336
+0.11101658 # (36, 9, 5) 168
+0.11097701 # (37, 5, 3) 336
+0.11089799 # (37, 6, 0) 288
+0.11085855 # (37, 6, 1) 528
+0.11077979 # (24, 24, 16) 24
+0.11074047 # (37, 6, 2) 432
+0.11070119 # (37, 5, 4) 384
+0.11066196 # (31, 21, 3) 96
+0.11062276 # (36, 10, 4) 192
+0.11058361 # (36, 9, 6) 360
+0.11054450 # (37, 6, 3) 336
+0.11046640 # (34, 16, 2) 192
+0.11042742 # (36, 11, 0) 192
+0.11038847 # (37, 7, 0) 408
+0.11034957 # (37, 7, 1) 288
+0.11031071 # (30, 22, 6) 96
+0.11027189 # (37, 6, 4) 648
+0.11023311 # (37, 7, 2) 288
+0.11015567 # (36, 8, 8) 144
+0.11011701 # (35, 14, 2) 336
+0.11007839 # (36, 11, 3) 384
+0.11003982 # (37, 7, 3) 360
+0.11000128 # (34, 16, 4) 96
+0.10996279 # (33, 18, 4) 264
+0.10992433 # (37, 6, 5) 384
+0.10984754 # (36, 10, 6) 72
+0.10980921 # (37, 8, 0) 432
+0.10977091 # (37, 8, 1) 384
+0.10973266 # (33, 15, 11) 96
+0.10965627 # (37, 8, 2) 288
+0.10961813 # (33, 18, 5) 192
+0.10954198 # (36, 12, 0) 120
+0.10950397 # (37, 6, 6) 336
+0.10946599 # (37, 8, 3) 288
+0.10942806 # (37, 7, 5) 192
+0.10939016 # (38, 0, 0) 126
+0.10935230 # (38, 1, 0) 456
+0.10931448 # (38, 1, 1) 384
+0.10923896 # (38, 2, 0) 216
+0.10920126 # (38, 2, 1) 576
+0.10916360 # (37, 9, 0) 168
+0.10912598 # (37, 9, 1) 312
+0.10908839 # (38, 2, 2) 104
+0.10905085 # (38, 3, 0) 168
+0.10901334 # (38, 3, 1) 720
+0.10893844 # (36, 12, 4) 48
+0.10890105 # (38, 3, 2) 288
+0.10886370 # (37, 8, 5) 324
+0.10882638 # (37, 9, 3) 264
+0.10878911 # (38, 4, 0) 240
+0.10875187 # (38, 4, 1) 336
+0.10871467 # (38, 3, 3) 192
+0.10864039 # (38, 4, 2) 144
+0.10860330 # (36, 13, 0) 192
+0.10856626 # (37, 9, 4) 696
+0.10852925 # (37, 7, 7) 120
+0.10845534 # (38, 5, 0) 672
+0.10841845 # (38, 5, 1) 432
+0.10830798 # (38, 5, 2) 336
+0.10827124 # (36, 13, 3) 192
+0.10823453 # (37, 9, 5) 360
+0.10819786 # (38, 4, 4) 288
+0.10816123 # (36, 10, 9) 192
+0.10812463 # (38, 5, 3) 360
+0.10805155 # (38, 6, 0) 144
+0.10801506 # (38, 6, 1) 624
+0.10797861 # (37, 8, 7) 288
+0.10794220 # (33, 15, 13) 168
+0.10790583 # (38, 6, 2) 192
+0.10786949 # (38, 5, 4) 384
+0.10783319 # (37, 9, 6) 240
+0.10776069 # (32, 20, 8) 48
+0.10772450 # (38, 6, 3) 240
+0.10768835 # (37, 11, 0) 432
+0.10765223 # (37, 11, 1) 288
+0.10761615 # (36, 14, 0) 120
+0.10758010 # (38, 7, 0) 264
+0.10754409 # (38, 7, 1) 600
+0.10747218 # (38, 6, 4) 336
+0.10743628 # (38, 7, 2) 240
+0.10740041 # (36, 11, 9) 192
+0.10736458 # (37, 11, 3) 312
+0.10729303 # (36, 14, 3) 288
+0.10725730 # (38, 7, 3) 288
+0.10718597 # (36, 12, 8) 96
+0.10715035 # (38, 6, 5) 480
+0.10711477 # (37, 11, 4) 288
+0.10707923 # (29, 21, 15) 96
+0.10704372 # (38, 8, 0) 192
+0.10700824 # (38, 8, 1) 480
+0.10697280 # (33, 15, 14) 192
+0.10690203 # (38, 8, 2) 192
+0.10686670 # (37, 12, 0) 192
+0.10683140 # (37, 12, 1) 600
+0.10679613 # (37, 11, 5) 288
+0.10676090 # (38, 6, 6) 72
+0.10672571 # (38, 8, 3) 576
+0.10669055 # (38, 7, 5) 288
+0.10658528 # (39, 0, 0) 390
+0.10655026 # (39, 1, 0) 240
+0.10651528 # (39, 1, 1) 168
+0.10648032 # (38, 8, 4) 240
+0.10644541 # (39, 2, 0) 360
+0.10641052 # (39, 2, 1) 576
+0.10634086 # (36, 14, 6) 96
+0.10630608 # (39, 2, 2) 624
+0.10627133 # (39, 3, 0) 432
+0.10623662 # (39, 3, 1) 264
+0.10616730 # (38, 8, 5) 288
+0.10613269 # (39, 3, 2) 240
+0.10606357 # (32, 16, 16) 24
+0.10602906 # (39, 4, 0) 192
+0.10599458 # (39, 4, 1) 528
+0.10596014 # (39, 3, 3) 408
+0.10592573 # (36, 12, 10) 96
+0.10589136 # (39, 4, 2) 432
+0.10585702 # (38, 7, 7) 240
+0.10578844 # (38, 10, 0) 240
+0.10575420 # (38, 10, 1) 288
+0.10571999 # (39, 5, 0) 408
+0.10568581 # (39, 5, 1) 288
+0.10565167 # (38, 10, 2) 120
+0.10561756 # (37, 12, 6) 216
+0.10558349 # (39, 5, 2) 672
+0.10551543 # (36, 16, 0) 48
+0.10548146 # (39, 4, 4) 480
+0.10544751 # (37, 13, 4) 288
+0.10541360 # (39, 5, 3) 96
+0.10537972 # (36, 16, 2) 264
+0.10534588 # (39, 6, 0) 504
+0.10531206 # (39, 6, 1) 144
+0.10524453 # (38, 10, 4) 192
+0.10521082 # (39, 6, 2) 384
+0.10517713 # (39, 5, 4) 336
+0.10514348 # (37, 13, 5) 144
+0.10507628 # (38, 11, 0) 336
+0.10504272 # (39, 6, 3) 576
+0.10497571 # (36, 16, 4) 108
+0.10494225 # (38, 11, 2) 336
+0.10490882 # (39, 7, 0) 240
+0.10487543 # (39, 7, 1) 408
+0.10484207 # (38, 8, 8) 144
+0.10480874 # (39, 6, 4) 264
+0.10477544 # (39, 7, 2) 648
+0.10470893 # (30, 26, 0) 120
+0.10467573 # (37, 12, 8) 336
+0.10464256 # (35, 17, 8) 192
+0.10460942 # (39, 7, 3) 216
+0.10457631 # (38, 10, 6) 192
+0.10454323 # (38, 11, 4) 480
+0.10451018 # (39, 6, 5) 192
+0.10444418 # (36, 12, 12) 72
+0.10441123 # (39, 8, 0) 288
+0.10437831 # (39, 8, 1) 528
+0.10434542 # (37, 13, 7) 200
+0.10431256 # (38, 12, 0) 72
+0.10427973 # (39, 8, 2) 624
+0.10424693 # (38, 11, 5) 384
+0.10418143 # (38, 12, 2) 240
+0.10414873 # (39, 6, 6) 192
+0.10411605 # (39, 8, 3) 408
+0.10408341 # (39, 7, 5) 384
+0.10401821 # (38, 12, 3) 168
+0.10398566 # (37, 15, 2) 384
+0.10392065 # (40, 0, 0) 30
+0.10388819 # (40, 1, 0) 672
+0.10385576 # (40, 1, 1) 480
+0.10382336 # (39, 9, 1) 144
+0.10379099 # (40, 2, 0) 240
+0.10375865 # (40, 2, 1) 192
+0.10372635 # (39, 9, 2) 336
+0.10366182 # (40, 2, 2) 192
+0.10362960 # (40, 3, 0) 336
+0.10359741 # (40, 3, 1) 384
+0.10356525 # (39, 9, 3) 360
+0.10353313 # (30, 26, 6) 48
+0.10350103 # (40, 3, 2) 504
+0.10346896 # (38, 13, 1) 336
+0.10340491 # (40, 4, 0) 168
+0.10337293 # (40, 4, 1) 336
+0.10334098 # (40, 3, 3) 144
+0.10330906 # (39, 7, 7) 360
+0.10327717 # (40, 4, 2) 216
+0.10324531 # (39, 10, 0) 216
+0.10321348 # (39, 10, 1) 360
+0.10314991 # (38, 12, 6) 192
+0.10311816 # (40, 5, 0) 576
+0.10308645 # (40, 5, 1) 240
+0.10305476 # (39, 9, 5) 168
+0.10299148 # (40, 5, 2) 600
+0.10295988 # (39, 10, 3) 336
+0.10289678 # (40, 4, 4) 48
+0.10286527 # (36, 16, 9) 192
+0.10283378 # (40, 5, 3) 768
+0.10280233 # (35, 19, 7) 192
+0.10277091 # (40, 6, 0) 192
+0.10273951 # (40, 6, 1) 456
+0.10270815 # (39, 9, 6) 432
+0.10264550 # (40, 6, 2) 192
+0.10261422 # (40, 5, 4) 528
+0.10258297 # (39, 11, 0) 168
+0.10255175 # (39, 11, 1) 240
+0.10252055 # (38, 14, 2) 144
+0.10248939 # (40, 6, 3) 192
+0.10245825 # (39, 11, 2) 528
+0.10236501 # (40, 7, 0) 576
+0.10233398 # (40, 7, 1) 480
+0.10230299 # (39, 11, 3) 192
+0.10227202 # (40, 6, 4) 240
+0.10224108 # (40, 7, 2) 192
+0.10221017 # (33, 23, 6) 264
+0.10214843 # (38, 14, 4) 240
+0.10211760 # (39, 10, 6) 192
+0.10208680 # (40, 7, 3) 504
+0.10205603 # (37, 17, 1) 192
+0.10199456 # (40, 6, 5) 576
+0.10196388 # (38, 13, 7) 240
+0.10190258 # (40, 8, 0) 72
+0.10187198 # (40, 8, 1) 576
+0.10184140 # (39, 12, 1) 336
+0.10181085 # (39, 11, 5) 312
+0.10178032 # (40, 8, 2) 144
+0.10174983 # (39, 12, 2) 312
+0.10171936 # (39, 10, 7) 336
+0.10165850 # (40, 6, 6) 96
+0.10162812 # (40, 8, 3) 384
+0.10159776 # (40, 7, 5) 576
+0.10156742 # (37, 15, 9) 168
+0.10153712 # (38, 14, 6) 216
+0.10150684 # (38, 13, 8) 192
+0.10147659 # (39, 11, 6) 240
+0.10141617 # (40, 8, 4) 96
+0.10138600 # (41, 0, 0) 246
+0.10135586 # (41, 1, 0) 372
+0.10132574 # (41, 1, 1) 240
+0.10129565 # (36, 18, 8) 120
+0.10126559 # (41, 2, 0) 624
+0.10123555 # (41, 2, 1) 528
+0.10117556 # (38, 12, 10) 120
+0.10114561 # (41, 2, 2) 432
+0.10111568 # (41, 3, 0) 312
+0.10108577 # (41, 3, 1) 432
+0.10102605 # (37, 18, 0) 264
+0.10099623 # (41, 3, 2) 624
+0.10093666 # (36, 20, 0) 72
+0.10090691 # (41, 4, 0) 336
+0.10087720 # (41, 4, 1) 192
+0.10084750 # (41, 3, 3) 264
+0.10081784 # (40, 10, 0) 336
+0.10078820 # (41, 4, 2) 624
+0.10075859 # (39, 10, 9) 240
+0.10069944 # (40, 10, 2) 288
+0.10066990 # (38, 15, 6) 192
+0.10064039 # (41, 5, 0) 696
+0.10061091 # (41, 5, 1) 240
+0.10058145 # (30, 22, 18) 48
+0.10055202 # (40, 10, 3) 504
+0.10052262 # (41, 5, 2) 240
+0.10046388 # (36, 20, 4) 72
+0.10043456 # (41, 4, 4) 432
+0.10040525 # (39, 12, 7) 240
+0.10037598 # (41, 5, 3) 384
+0.10034673 # (40, 10, 4) 192
+0.10031750 # (41, 6, 0) 192
+0.10028830 # (41, 6, 1) 552
+0.10022998 # (36, 18, 10) 144
+0.10020085 # (41, 6, 2) 624
+0.10017175 # (41, 5, 4) 288
+0.10014268 # (39, 11, 9) 120
+0.10008461 # (40, 11, 2) 480
+0.10005561 # (41, 6, 3) 288
diff --git a/src/geomtools/powder/calibrants/__init__.py b/src/geomtools/powder/calibrants/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..cbbc627f5e40ae0ed209b71996a27e93c8a9aa5e
--- /dev/null
+++ b/src/geomtools/powder/calibrants/__init__.py
@@ -0,0 +1,16 @@
+from importlib.resources import files
+
+import numpy as np
+
+calibrants = files("geomtools.powder.calibrants")
+
+
+def get_calibrant(name, inverse=True):
+    fn = calibrants.joinpath(f"{name}.D")
+    d = np.loadtxt(str(fn)) / 10  # Ã… -> nm
+    return 1. / d if inverse else d
+
+
+def calibrant_list():
+    return [f.stem for f in calibrants.iterdir()
+            if f.suffix == ".D"]
diff --git a/src/geomtools/powder/find_center.py b/src/geomtools/powder/find_center.py
new file mode 100644
index 0000000000000000000000000000000000000000..1df6428374f05921167fc5c966f4bc4b93b6e4f7
--- /dev/null
+++ b/src/geomtools/powder/find_center.py
@@ -0,0 +1,124 @@
+import os
+from argparse import ArgumentParser
+
+import h5py
+import matplotlib.pyplot as plt
+from extra.components import AGIPD1MQuadrantMotors
+from extra_data import open_run
+from extra_geom import AGIPD_1MGeometry, JUNGFRAUGeometry
+from extra_geom.motors import AGIPD_1MMotors
+from . import PowderDiffraction
+from .calibrants import calibrant_list, get_calibrant
+from .misc import agipd_borders, jungfrau_borders
+
+DETECTOR_TYPE = {
+    "agipd1m": AGIPD_1MGeometry,
+    "jf4m": JUNGFRAUGeometry,
+}
+
+BORDER_MASK = {
+    "agipd1m": agipd_borders,
+    "jf4m": jungfrau_borders,
+}
+
+CALIBRANTS = calibrant_list()
+
+
+def guess_detector_type(detector_id):
+    for detector_type in DETECTOR_TYPE.keys():
+        if detector_type.upper() in detector_id:
+            return detector_type
+
+    raise ValueError("Unknown detector type")
+
+
+def main(argv=None):
+    parser = ArgumentParser(
+        description="The program finds the beam position by "
+        "powder diffraction data")
+
+    parser.add_argument("-g", "--geometry", required=True,
+                        help="Crystfel geometry file")
+    parser.add_argument("-l", "--layout", choices=DETECTOR_TYPE.keys(),
+                        help="Detector layout")
+    parser.add_argument("-c", "--calibrant", required=True,
+                        choices=CALIBRANTS, help="Calibrant")
+    parser.add_argument("-o", "--output", default=".",
+                        help="Output directory")
+    parser.add_argument("powder_data",
+                        help="Averaged powder diffraction data")
+
+    args = parser.parse_args(argv)
+
+    with h5py.File(args.powder_data, "r") as f:
+        # conditions
+        clen = f["powderSum/conditions/cameraLen"][()]
+        photon_en = f["powderSum/conditions/photonEnergy"][()]
+        lmd = f["powderSum/conditions/waveLength"][()]
+        detector_id = f["powderSum/conditions/detectorId"][()].decode()
+        runno = f["powderSum/conditions/run"][()]
+        propno = f["powderSum/conditions/proposal"][()]
+
+        # average image
+        img_mean = f["powderSum/image/mean"][:]
+        num_frames = f["powderSum/image/numFrames"][:]
+        mask = f["powderSum/image/mask"][:].astype(bool)
+        modules = f["powderSum/image/modules"][:]
+
+    num_modules = len(modules)
+
+    print("Proposal:", propno)
+    print("Run:", runno)
+
+    if args.layout is None:
+        detector_type = guess_detector_type(detector_id)
+    else:
+        detector_type = args.layout
+
+    print("Detector Id:", detector_id)
+    print("Detector type:", detector_type)
+
+    print("Photon energy", photon_en, "(keV)")
+    print("Wave length", lmd, "(nm)")
+    print("Camera length", clen, "(m)")
+
+    geom_class = DETECTOR_TYPE[detector_type]
+    ref_geom = geom_class.from_crystfel_geom(args.geometry)
+
+    if detector_type == "agipd1m":
+        run = open_run(propno, runno, data="raw")
+        tracker = AGIPD_1MMotors(ref_geom)
+        motors = AGIPD1MQuadrantMotors(run)
+        geom = tracker.geom_at(motors.most_frequent_positions())
+    else:
+        geom = ref_geom
+
+    rings = get_calibrant(args.calibrant)
+    borders = BORDER_MASK[detector_type](num_modules)
+    pw = PowderDiffraction(geom, rings, clen, lmd,
+                           border_mask=~borders, make_shadow_mask=True)
+    pw.fit(img_mean, mask, num_frames)
+    pw.refinement_info()
+
+    geom_new = pw.transform_geometry()
+    refined_geom_fn = os.path.join(
+        args.output, f"powder_center_p{propno:06d}_r{runno:04d}.geom")
+    geom_new.write_crystfel_geom(
+        refined_geom_fn,
+        clen=geom_new.metadata["clen"],
+        photon_energy=geom_new.metadata["photon_energy"],
+        adu_per_ev=geom_new.metadata["photon_energy"] / 1000,
+    )
+
+    fig, (ax1, ax2) = plt.subplots(
+        2, 1, figsize=(12, 16), tight_layout=True,
+        gridspec_kw={'height_ratios': [3, 1]})
+    pw.draw(ax=ax1)
+    pw.draw_radial_profile(img_mean, mask, ax=ax2)
+    report_img_fn = os.path.join(
+        args.output, f"powder_center_p{propno:06d}_r{runno:04d}.png")
+    fig.savefig(report_img_fn, dpi=300)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/geomtools/powder/image_tools.py b/src/geomtools/powder/image_tools.py
new file mode 100644
index 0000000000000000000000000000000000000000..a492c8756bd6e4a6bb2a20801844a6cb130244ef
--- /dev/null
+++ b/src/geomtools/powder/image_tools.py
@@ -0,0 +1,100 @@
+import numpy as np
+from scipy.ndimage import gaussian_filter
+from skimage import measure
+
+from .misc import azint_fast, pixel_radii
+
+
+def make_shadow_mask(img, bad_pixels=None, threshold=0.5,
+                     max_noise_spot_size=100):
+    """Makes shadow mask."""
+    max_bad_spot_size = 10000
+    if bad_pixels is None:
+        bad_pixels = np.zeros_like(img, dtype=bool)
+    mask = img > threshold
+    for modno in range(len(mask)):
+        mod_labels = measure.label(mask[modno], background=0)
+        area = np.bincount(mod_labels.ravel())
+
+        for ix in range(1, len(area)):
+            if area[ix] < max_noise_spot_size:
+                mask[modno][mod_labels == ix] = False
+
+        mod_labels = measure.label(mask[modno], background=1)
+        area = np.bincount(mod_labels.ravel())
+
+        for ix in range(1, len(area)):
+            if area[ix] < max_noise_spot_size:
+                mask[modno][mod_labels == ix] = True
+            elif area[ix] < max_bad_spot_size:
+                flag = mod_labels == ix
+                mask[modno][flag] = ~bad_pixels[modno][flag]
+
+    return ~mask
+
+
+def threshold_gaussian(geom, image, bad_pixels=None, shadow=None,
+                       sigma=7, snr=1, return_background=False):
+    """Binarize image."""
+    if bad_pixels is None:
+        bad_pixels = np.zeros_like(image, dtype=bool)
+    if shadow is None:
+        shadow = np.zeros_like(image, dtype=bool)
+
+    ri = pixel_radii(geom, dtype=int)
+    v = azint_fast(ri, image, shadow | bad_pixels)
+
+    mask = bad_pixels & ~shadow
+
+    img = np.copy(image)
+    img[mask] = v[ri[mask]]
+    img[shadow] = 0
+
+    shadow_ext = (
+        gaussian_filter(shadow.astype(float), sigma, axes=(1, 2)) > 0.01)
+
+    lg_im = np.log(img)
+    minv = np.min(lg_im[~shadow_ext])
+    lg_im[~np.isfinite(lg_im)] = minv
+
+    mu = gaussian_filter(lg_im, sigma, axes=(1, 2))
+    sig = gaussian_filter(lg_im * lg_im, sigma, axes=(1, 2))
+    sig = np.sqrt(sig - mu * mu)
+
+    fg = lg_im > mu + snr * sig
+    fg = fg & ~(bad_pixels | shadow_ext)
+
+    return (fg, mu, sig) if return_background else fg
+
+
+def find_rings(r, img, peaks, min_area=30):
+    r_min = peaks[0] / 2
+
+    labels = np.zeros_like(img, dtype=int)
+    num_images = len(img)
+    for modno in range(num_images):
+        mod_labels = labels[modno]
+        mod_labels[:] = measure.label(img[modno], background=0)
+        area = np.bincount(mod_labels.ravel())
+
+        for ix in range(1, len(area)):
+            if area[ix] < min_area:
+                mod_labels[mod_labels == ix] = False
+
+        for label in np.unique(mod_labels):
+            if label == 0:
+                continue
+
+            px = mod_labels == label
+            rr = r[modno][px]
+
+            r_mean = np.mean(rr)
+            dr = np.abs(peaks - r_mean)
+
+            peak_ix = np.argmin(dr)
+            if dr[peak_ix] > r_min:
+                mod_labels[px] = 0
+            else:
+                mod_labels[px] = peak_ix + 1
+
+    return labels
diff --git a/src/geomtools/powder/misc.py b/src/geomtools/powder/misc.py
new file mode 100644
index 0000000000000000000000000000000000000000..76a7e9ef3f02509a8ee31adb2cebe37e3f97c494
--- /dev/null
+++ b/src/geomtools/powder/misc.py
@@ -0,0 +1,69 @@
+import numpy as np
+
+
+def azint_fast(ri, img, mask=None, nan=np.nan):
+    if mask is None:
+        r, v = ri, img
+    else:
+        r = ri[~mask]
+        v = img[~mask]
+    n = np.bincount(r)
+    rdf = np.bincount(r, weights=v)
+    np.divide(rdf, n, where=n > 0, out=rdf)
+    rdf[n == 0] = nan
+    return rdf
+
+
+def pixel_radii(geom, dtype=None):
+    xyz = geom.get_pixel_positions() / geom.pixel_size
+    x = xyz[..., 0]
+    y = xyz[..., 1]
+    r = np.sqrt(x * x + y * y)
+    if dtype is not None:
+        r = r.astype(dtype)
+    return r
+
+
+def reciprocal_to_dectector(s, lmd, clen):
+    # s = q / (2pi)
+    # theta = 2. * np.arcsin(lmd * s / 2.)  # theta = 2arcsin(lmd * q / 4pi)
+    # r = (clen / pixel_size) * np.tan(theta)  # r = L * tan(theta)
+    lmd_s = lmd * s
+    lmd_s_2 = lmd_s * lmd_s
+    r = clen * lmd_s * np.sqrt(4.0 - lmd_s_2) / (2.0 - lmd_s_2)
+    return r
+
+
+def agipd_borders(num_modules=16):
+    borders = np.zeros([num_modules, 512, 128], bool)
+    borders[:, 0::64, :] = True
+    borders[:, 63::64, :] = True
+    borders[:, :, 0] = True
+    borders[:, :, 127] = True
+    return borders
+
+
+def agipd_pixel_area():
+    px_area = np.ones([512, 128], np.float32)
+    px_area[63::64, :] = 2.0
+    px_area[64::64, :] = 2.0
+    return px_area
+
+
+def jungfrau_borders(num_modules=8):
+    borders = np.zeros([num_modules, 512, 1024], bool)
+    borders[:, :, 0::256] = True
+    borders[:, :, 255::256] = True
+    borders[:, 0::256, :] = True
+    borders[:, 255::256, :] = True
+    return borders
+
+
+def jungfrau_pixel_area():
+    fs = np.ones(512, np.float32)
+    fs[255::256] = 2.0
+    fs[256::256] = 2.0
+    ss = np.ones(1024, np.float32)
+    ss[255::256] = 2.0
+    ss[256::256] = 2.0
+    return np.outer(fs, ss)
diff --git a/src/geomtools/powder/powder.py b/src/geomtools/powder/powder.py
new file mode 100644
index 0000000000000000000000000000000000000000..af7cb67eb87a8f6880ea42a50fdd1bbb82f9504e
--- /dev/null
+++ b/src/geomtools/powder/powder.py
@@ -0,0 +1,146 @@
+import matplotlib.pyplot as plt
+import numpy as np
+from pyFAI.azimuthalIntegrator import AzimuthalIntegrator
+
+from .image_tools import find_rings, make_shadow_mask, threshold_gaussian
+from .misc import pixel_radii, reciprocal_to_dectector
+from .refine import fit_rings, refine_clen
+
+
+class PowderDiffraction:
+    def __init__(self, geom, peaks, clen, wave_length,
+                 min_area=30, sigma=7, snr=1, border_mask=None,
+                 make_shadow_mask=True):
+        self.geom = geom
+        self.peaks = peaks
+        self.clen = clen
+        self.lmd = wave_length
+        self.min_area = min_area
+        self.sigma = sigma
+        self.snr = snr
+        self.make_shadow_mask = make_shadow_mask
+        self.labels = None
+        self.beam_center = None
+        self.shadow = None
+        if border_mask is None:
+            border_mask = np.ones(geom.expected_data_shape, bool)
+        self.border_mask = border_mask
+
+    def fit(self, img, mask, num_images=1):
+        if self.make_shadow_mask:
+            threshold = 1.0 / np.sqrt(num_images)
+            if np.size(threshold) > 1:
+                slice_thr = (slice(None),) + (None,) * (img.ndim - 1)
+                threshold = threshold[slice_thr]
+            self.shadow = make_shadow_mask(img, ~mask, threshold)
+
+        fg = threshold_gaussian(
+            self.geom, img, mask, self.shadow, self.sigma, self.snr)
+
+        fg = fg & self.border_mask
+
+        xyz = self.geom.get_pixel_positions()
+        ri = np.linalg.norm(xyz[..., :2], axis=-1)
+
+        peaks = reciprocal_to_dectector(self.peaks, self.lmd, self.clen)
+        self.labels = find_rings(ri, fg, peaks, self.min_area)
+        res = fit_rings(xyz[..., :2], self.labels, peaks)
+
+        self.beam_center = (float(res[0]), float(res[1]))
+        self.peak_no = res[2]
+        self.peak_radii = res[3]
+
+        self.clen_hat = refine_clen(
+            self.peaks, self.lmd, xyz[..., :2], self.beam_center, self.labels
+        )
+        self.lmd_hat = self.lmd
+        self.peak_radii /= self.geom.pixel_size
+
+    def transform_geometry(self):
+        if self.beam_center is None:
+            raise ValueError("Rings are not fitted yet.")
+
+        xc, yc = self.beam_center
+        geom = self.geom.offset([-xc, -yc])
+
+        hc_e = 1.2398419843320026
+        geom.metadata.update(
+            {
+                "clen": self.clen_hat,
+                "photon_energy": hc_e / self.lmd_hat * 1000,
+            }
+        )
+        motor_positions = getattr(self, "motor_positions", None)
+        if motor_positions is not None:
+            geom.motor_positions = motor_positions
+        return geom
+
+    def draw(self, ax=None):
+        geom = self.transform_geometry()
+
+        cm = plt.cm.Set1.copy()
+        cm.set_under([0.3, 0.3, 0.3])
+        cm.set_over([0, 0, 0])
+        lab_draw = np.copy(self.labels)
+        bg = lab_draw == 0
+        lab_draw = (lab_draw - 1) % cm.N + 1
+        lab_draw[bg] = cm.N + 1
+        ax = geom.plot_data(lab_draw, ax=ax, colorbar=False, cmap=cm,
+                            interpolation="none", vmin=1, vmax=cm.N)
+
+        peaks = reciprocal_to_dectector(
+            self.peaks[self.peak_no - 1], self.lmd_hat,
+            self.clen_hat / geom.pixel_size)
+
+        for r in peaks:
+            ax.add_patch(
+                plt.Circle((0, 0), r, fill=False, ls=":", lw=1, ec="w"))
+
+        return ax
+
+    def refinement_info(self):
+        if self.beam_center is None:
+            raise ValueError("Rings are not fitted yet.")
+        hc_e = 1.2398419843320026
+        xc, yc = np.array(self.beam_center) / self.geom.pixel_size
+        print(f"Beam center: ({xc:.2f}, {yc:.2f}) px")
+        print(f"Camera len: {self.clen:.4f} -> {self.clen_hat:.4f} (m)")
+        print(f"Photon energy: {hc_e / self.lmd_hat:.3f} eV")
+
+    def draw_radial_profile(self, img, mask, ax=None, figsize=(10, 5)):
+        geom = self.transform_geometry()
+
+        peaks = reciprocal_to_dectector(
+            self.peaks, self.lmd_hat, self.clen_hat * 1000
+        )  # in mm
+        peaks = peaks[:max(self.peak_no)]
+
+        ai = AzimuthalIntegrator(
+            detector=geom.to_pyfai_detector(),
+            dist=self.clen_hat,  # sample-detector distance (m)
+            wavelength=self.lmd_hat * 1e-10,  # wavelength (m)
+        )
+
+        rmin = np.min(pixel_radii(geom)[~mask]) * geom.pixel_size * 1000
+
+        msk = mask
+        if self.shadow is not None:
+            msk |= self.shadow
+
+        rint, intens = ai.integrate1d(
+            img.reshape(-1, 128),
+            mask=msk.reshape(-1, 128),
+            npt=1000,
+            unit="r_mm",
+            radial_range=(rmin, peaks[-1] + 1),
+        )
+        if ax is None:
+            fig, ax = plt.subplots(1, 1, figsize=figsize)
+
+        for r in peaks:
+            ax.axvline(r, c="gray", lw=0.5, ls=":")
+
+        ax.semilogy(rint, intens)
+        ax.set_xlim(rmin, peaks[-1] + 1)
+
+        return ax
diff --git a/src/geomtools/powder/powdersum.py b/src/geomtools/powder/powdersum.py
new file mode 100644
index 0000000000000000000000000000000000000000..bea57e684e1c5e7b1c1dbab255094cc2dba5b802
--- /dev/null
+++ b/src/geomtools/powder/powdersum.py
@@ -0,0 +1,356 @@
+import multiprocessing as mp
+import os
+import re
+import time
+from argparse import ArgumentParser
+
+import h5py
+import numpy as np
+import psutil
+from extra_data import RunDirectory, open_run
+from extra_data.read_machinery import find_proposal
+
+from . import shmemarray as shmem
+from .misc import agipd_pixel_area, jungfrau_pixel_area
+
+SASE = {
+    "SA1": "SA1", "FXE": "SA1", "SPB": "SA1",
+    "SA2": "SA1", "HED": "SA2", "MID": "SA2",
+    "SA3": "SA3", "SCS": "SA3", "SQS": "SA3", "SXP": "SA3",
+}
+
+XGM = {
+    "SA1": "SA1_XTD2_XGM/XGM/DOOCS",
+    "SA2": "",
+    "SA3": "",
+}
+
+ZMOTOR = {
+    "SPB_DET_AGIPD1M-1": (
+        "SPB_IRU_AGIPD1M1/MDL/DATA_SELECTOR",
+        "spbIruAgipd1MMotorZStepper.actualPosition",
+    ),
+}
+MIN_CLEN = {"SPB_DET_AGIPD1M-1": 0.118}
+
+PX_AREA = {
+    "agipd1m": agipd_pixel_area,
+    "jf4m": jungfrau_pixel_area,
+}
+
+
+class ImageAgg:
+    def __init__(
+        self,
+        nproc,
+        det_source,
+        shape,
+        adu_per_unit=1,
+        rounding_threshold=None,
+        px_area=None,
+        mask=None,
+    ):
+        self.det_source = det_source
+        self.adu_per_unit = adu_per_unit
+        if rounding_threshold is None:
+            self.rounding = False
+            self.rounding_shift = 0.0
+        else:
+            self.rounding = True
+            self.rounding_shift = rounding_threshold - 0.5
+        self.shape = shape
+        self.nproc = nproc
+        if mask is None:
+            mask = np.zeros(shape, int)
+
+        self.mask = mask == 0
+        arr_shape = (nproc,) + tuple(shape)
+        if px_area is None:
+            px_area = np.ones(shape, np.float32)
+
+        self.px_area = px_area
+
+        self.part_mean = shmem.empty(arr_shape, float)
+        self.part_deviation = shmem.empty(arr_shape, float)
+        self.part_count = shmem.empty(arr_shape, int)
+        self.part_num_frames = shmem.empty(nproc, int)
+
+    def compute(self, args):
+        i, dc = args
+        mean = self.part_mean[i]
+        mean[:] = 0
+        deviation = self.part_deviation[i]
+        deviation[:] = 0
+        count = self.part_count[i]
+        count[:] = 0
+        self.part_num_frames[i] = np.sum(
+            dc[self.det_source].data_counts(index_group="image",
+                                            labelled=False)
+        )
+
+        for tid, data in dc.trains():
+            a = data[self.det_source]["image.data"] / self.adu_per_unit
+            msk = (data[self.det_source]["image.mask"] == 0) & self.mask
+
+            a[~msk] = 0.0
+            if self.rounding:
+                msk &= a > -self.rounding_shift - 0.5
+                np.round(a - self.rounding_shift, out=a)
+                np.clip(a, 0.0, None, out=a)
+
+            mean += np.sum(a, 0)
+            deviation += np.sum(a * a, 0)
+            count += np.sum(msk.astype(int), 0)
+        return i
+
+    def finish(self):
+        self.r = type("Moments", (), {})
+        self.count = np.sum(self.part_count, 0)
+        nz = self.count > 0
+
+        self.mean = np.zeros(self.shape, np.float32)
+        self.mean = np.divide(
+            np.sum(self.part_mean, 0), self.count,
+            out=self.mean, where=nz
+        )
+        self.mean /= self.px_area
+
+        self.deviation = np.zeros(self.shape, np.float32)
+        self.deviation = np.divide(
+            np.sum(self.part_deviation, 0), self.count,
+            out=self.deviation, where=nz
+        )
+        self.deviation = np.sqrt(self.deviation - self.mean * self.mean)
+        self.deviation /= self.px_area
+
+        self.num_frames = np.sum(self.part_num_frames)
+
+
+def main(argv=None):
+    parser = ArgumentParser(
+        description="The program sum up images over a given run")
+    parser.add_argument("-n", "--num-proc", type=int,
+                        help="The number of processes")
+    parser.add_argument("-p", "--proposal", type=int, required=True,
+                        help="proposal no.")
+    parser.add_argument("-r", "--run", type=int, required=True, help="run no.")
+    parser.add_argument("-D", "--images-dir",
+                        help="Directory with calibrated detector images "
+                             "if different")
+    parser.add_argument("--legacy", action="store_true",
+                        help="Use legacy naming for corrected detector "
+                             "sources")
+    parser.add_argument("-X", "--xgm-id", help="XGM device Id")
+    parser.add_argument("--z-motor-id", help="Detector z-axis motor Id")
+    parser.add_argument("--z-motor-key", default="activePosition",
+                        help="Detector z-axis motor Id")
+    parser.add_argument("-s", "--spacing-len", type=float, default=0.0,
+                        help="Length of additional spacing tubes (m)")
+    parser.add_argument("-L", "--camera-len", type=float,
+                        help="Sample-to-detector distance (m)")
+    parser.add_argument("-R", "--round-to-photons", action="store_true",
+                        help="Convert and round intensities to photons")
+    parser.add_argument("-t", "--rounding-threshold", type=float, default=0.7,
+                        help="The rounding threshold")
+    parser.add_argument("-P", "--photon-energy", type=float,
+                        help="The rounding threshold")
+    parser.add_argument("-M", "--modules",
+                        type=lambda s: eval(f"np.r_[{s}]").tolist(),
+                        help="List of modules")
+    parser.add_argument("-l", "--layout", choices=PX_AREA.keys(),
+                        help="Detector layout")
+    parser.add_argument("-m", "--mask", help="Bad pixel mask filename")
+    parser.add_argument("--mask-path", default="/entry_1/data_1/mask",
+                        help="Data source path in h5 file",)
+    parser.add_argument("-o", "--output-dir",
+                        help="Directory to write computed images")
+    parser.add_argument("detector_id", help="Detector Id")
+
+    args = parser.parse_args(argv)
+
+    propdir = find_proposal(f"p{args.proposal:06d}")
+
+    if args.num_proc is None:
+        num_proc = min(psutil.cpu_count(logical=False), 32)
+
+    detector_id = args.detector_id
+    inst, _, _ = detector_id.partition("_")
+    print("Instrument:", inst)
+
+    run = open_run(args.proposal, args.run, data="all")
+
+    # photon energy and wave length
+    hc_e = 1.2398419843320026
+    photon_en = args.photon_energy
+    if photon_en:
+        # wave length
+        lmd = hc_e / photon_en
+    else:
+        xgm_id = args.xgm_id
+        if not xgm_id:
+            xgm_id = XGM[SASE[inst]]
+        print("XGM:", xgm_id)
+
+        # wave length
+        lmd = run[xgm_id, "pulseEnergy.wavelengthUsed"].as_single_value()
+        # photon energy
+        photon_en = hc_e / lmd
+
+    # camera length
+    clen = args.camera_len
+    if clen is None:
+        # read from motors
+        z_motor_id = args.z_motor_id
+        if z_motor_id:
+            z_motor_key = args.z_motor_key
+        else:
+            z_motor_id, z_motor_key = ZMOTOR[detector_id]
+
+        print(f"Detector Z motor: {z_motor_id}.{z_motor_key}")
+
+        z_motor = run[z_motor_id]
+        clen0 = MIN_CLEN.get(detector_id)
+        clen = 1e-3 * z_motor.run_value(z_motor_key) + clen0 + args.spacing_len
+
+    print()
+    print(f"Photon energy: {photon_en:g} (keV)")
+    print(f"Wave length: {lmd:.4g} (nm)")
+    print(f"Camera length: {clen:.3f} (m)")
+
+    if args.images_dir:
+        images_dir = args.images_dir
+        if images_dir[0] == "^":
+            images_dir = os.path.join(propdir, images_dir[1:])
+        try_add_rundir = os.path.join(images_dir, f"r{args.run:04d}")
+        if os.path.isdir(try_add_rundir):
+            images_dir = try_add_rundir
+        dc_img = RunDirectory(images_dir)
+    else:
+        dc_img = run
+
+    # detector sources
+    source_pattern = "{detector_id}/CORR/{modno}CH0:output"
+    key_pattern = "image.*"
+
+    det_source_pattern = source_pattern.format(
+        detector_id=detector_id, modno="*")
+    if args.legacy:
+        det_source_pattern = det_source_pattern.replace("/CORR/", "/DET/")
+        det_source_pattern = det_source_pattern.replace(":output", ":xtdf")
+
+    dc_img = dc_img.select([(det_source_pattern, key_pattern)])
+
+    modules = args.modules
+    if modules is None:
+        re_pattern = re.compile(
+            "^" +
+            source_pattern.format(
+                detector_id=detector_id, modno=r"(?P<modno>\d+)") +
+            "$"
+        )
+        matches = (re_pattern.match(s) for s in dc_img.all_sources)
+        modules = sorted(int(m["modno"]) for m in matches if m is not None)
+    num_modules = len(modules)
+
+    if args.mask:
+        mask_fn = args.mask
+        if mask_fn[0] == "^":
+            mask_fn = os.path.join(propdir, mask_fn[1:])
+
+        with h5py.File(mask_fn, "r") as f:
+            mask0 = f[args.mask_path][:]
+    else:
+        mask_fn = None
+        mask0 = [None] * num_modules
+
+    print()
+    print("Detector source pattern:", det_source_pattern)
+    print("Num modules:", num_modules)
+    print("Modules:", modules)
+    print("Mask:", mask_fn)
+    print()
+    print("Num proc:", num_proc)
+
+    if args.round_to_photons:
+        adu_per_unit = photon_en
+        rounding_threshold = args.rounding_threshold
+        print("Rounding threshold:", rounding_threshold)
+    else:
+        adu_per_unit = 1
+        rounding_threshold = None
+
+    pw_aggs = []
+    for modi, modno in enumerate(modules):
+        tm0 = time.perf_counter()
+        print(f"{modno: 3d}: ", end="")
+
+        source_id = source_pattern.format(detector_id=detector_id, modno=modno)
+        dc = dc_img.select(
+            [(source_id, "image.data"), (source_id, "image.mask")],
+            require_all=True
+        )
+
+        shape = dc[source_id, "image.data"].shape[1:]
+        if args.layout is None:
+            px_area = None
+        else:
+            px_area = PX_AREA[args.layout]()
+
+        agg = ImageAgg(
+            num_proc,
+            source_id,
+            shape,
+            adu_per_unit,
+            rounding_threshold,
+            px_area=px_area,
+            mask=mask0[modi],
+        )
+
+        with mp.Pool(num_proc) as pool:
+            result_iter = pool.imap_unordered(
+                agg.compute, enumerate(dc.split_trains(num_proc))
+            )
+
+            for r in result_iter:
+                pass
+
+        agg.finish()
+        pw_aggs.append(agg)
+
+        tm1 = time.perf_counter()
+        print(f"{tm1 - tm0:.1f} s")
+
+    output_fn = f"powder_sum_p{args.proposal:06d}_r{args.run:04d}.h5"
+    if args.output_dir:
+        output_fn = os.path.join(args.output_dir, output_fn)
+    with h5py.File(output_fn, "w") as f:
+        # conditions
+        f["powderSum/conditions/cameraLen"] = clen
+        f["powderSum/conditions/photonEnergy"] = photon_en
+        f["powderSum/conditions/waveLength"] = lmd
+        f["powderSum/conditions/detectorId"] = detector_id.encode("ascii")
+        f["powderSum/conditions/run"] = args.run
+        f["powderSum/conditions/proposal"] = args.proposal
+
+        # average image
+        f["powderSum/image/mean"] = np.stack(
+            [pw_aggs[i].mean for i in range(num_modules)]
+        )
+        f["powderSum/image/std"] = np.stack(
+            [pw_aggs[i].deviation for i in range(num_modules)]
+        )
+        f["powderSum/image/count"] = np.stack(
+            [pw_aggs[i].count for i in range(num_modules)]
+        )
+        f["powderSum/image/numFrames"] = np.stack(
+            [pw_aggs[i].num_frames for i in range(num_modules)]
+        )
+        f["powderSum/image/mask"] = np.stack(
+            [~pw_aggs[i].mask | (pw_aggs[i].count == 0)
+             for i in range(num_modules)]
+        ).astype(np.uint8)
+        f["powderSum/image/modules"] = modules
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/geomtools/powder/refine.py b/src/geomtools/powder/refine.py
new file mode 100644
index 0000000000000000000000000000000000000000..e171fe317c594b872a9321b2884dcc053f74fb71
--- /dev/null
+++ b/src/geomtools/powder/refine.py
@@ -0,0 +1,75 @@
+import numpy as np
+
+from .misc import reciprocal_to_dectector
+
+
+def q_moments(x, y):
+    n = x.size
+
+    xx = x * x
+    yy = y * y
+    xy = x * y
+    f = xx + yy
+
+    Mx = np.sum(x)
+    My = np.sum(y)
+    Mf = np.sum(f)
+    Mxx = np.sum(xx)
+    Mxy = np.sum(xy)
+    Myy = np.sum(yy)
+    Mff = np.sum(f * f)
+    Mfx = np.sum(f * x)
+    Mfy = np.sum(f * y)
+
+    return type("PointMoments", (), dict(
+        n=n, x=Mx, y=My, f=Mf, xx=Mxx, yy=Myy,
+        ff=Mff, xy=Mxy, fx=Mfx, fy=Mfy
+    ))
+
+
+def fit_rings(xy, labels, peaks):
+    x = xy[..., 0]
+    y = xy[..., 1]
+
+    found_peaks = np.unique(labels)
+    n = len(found_peaks) + 1
+    W = np.zeros([n, n], dtype=float)
+    E = np.zeros(n, dtype=float)
+
+    for i in range(n - 2):
+        peak_no = found_peaks[i + 1]
+
+        pixels = labels == peak_no
+        M = q_moments(x[pixels], y[pixels])
+
+        j = 2 + i
+        W[i, :2] = [2.0 * M.x, 2.0 * M.y]
+        W[-2:, j] = [M.x, M.y]
+        W[i, j] = M.n
+        W[-2:, :2] += [[2 * M.xx, 2 * M.xy], [2 * M.xy, 2 * M.yy]]
+
+        E[i] = M.f
+        E[-2:] += [M.fx, M.fy]
+
+    # WU = E
+    U = np.linalg.solve(W, E)
+    xc, yc = U[0], U[1]
+
+    peak_r_hat = np.sqrt(U[2:] + xc * xc + yc * yc)
+    peak_no = found_peaks[1:]
+
+    return xc, yc, peak_no, peak_r_hat
+
+
+def refine_clen(s, lmd, xy, center, labels):
+    xc, yc = center
+    x = xy[..., 0] - xc
+    y = xy[..., 1] - yc
+
+    m = labels > 0
+    r_hat_2 = x[m] * x[m] + y[m] * y[m]
+    ix = labels[m] - 1
+
+    r = reciprocal_to_dectector(s[ix], lmd, 1.0)
+    clen_hat = np.sqrt(np.sum(r_hat_2) / np.sum(r * r))
+    return clen_hat
diff --git a/src/geomtools/powder/shmemarray.py b/src/geomtools/powder/shmemarray.py
new file mode 100644
index 0000000000000000000000000000000000000000..a03e603e6bc45dd8a75a65b348b4ba719cba69a2
--- /dev/null
+++ b/src/geomtools/powder/shmemarray.py
@@ -0,0 +1,144 @@
+import mmap
+
+import numpy
+from packaging.version import Version
+
+NUMPY_LT_2_0 = not (Version(numpy.__version__) < Version("2.0.dev"))
+
+
+def empty_like(array, dtype=None):
+    """Create a shared memory array from the shape of array."""
+    array = numpy.asarray(array)
+    if dtype is None:
+        dtype = array.dtype
+    return anonymousmemmap(array.shape, dtype)
+
+
+def empty(shape, dtype="f8"):
+    """Create an empty shared memory array."""
+    return anonymousmemmap(shape, dtype)
+
+
+def full_like(array, value, dtype=None):
+    """Create a shared memory array with the same shape and type
+    as a given array, filled with `value`."""
+    shared = empty_like(array, dtype)
+    shared[:] = value
+    return shared
+
+
+def full(shape, value, dtype="f8"):
+    """Create a shared memory array of given shape and type,
+    filled with `value`."""
+    shared = empty(shape, dtype)
+    shared[:] = value
+    return shared
+
+
+def copy(a):
+    """Copy an array to the shared memory.
+
+    Notes
+    -----
+    copy is not always necessary because the private memory is
+    always copy-on-write.
+
+    Use :code:`a = copy(a)` to immediately dereference the old 'a'
+    on private memory
+    """
+    shared = anonymousmemmap(a.shape, dtype=a.dtype)
+    shared[:] = a[:]
+    return shared
+
+
+def fromiter(iter, dtype, count=None):
+    return copy(numpy.fromiter(iter, dtype, count))
+
+
+try:
+    # numpy >= 1.16
+    _unpickle_ctypes_type = numpy.ctypeslib.as_ctypes_type(numpy.dtype("|u1"))
+except Exception:
+    # older version numpy < 1.16
+    _unpickle_ctypes_type = numpy.ctypeslib._typecodes["|u1"]
+
+
+def __unpickle__(ai, dtype):
+    dtype = numpy.dtype(dtype)
+    tp = _unpickle_ctypes_type
+
+    # The following lb ub logic replaces ctypeslib.as_ctypes()
+    # that does not support strides.
+    if ai["strides"]:
+        lb = 0
+        ub = dtype.itemsize
+        for s, t in zip(ai["strides"], ai["shape"]):
+            if t == 0:  # there is no data if any shape is 0.
+                lb, ub = 0, 0
+                break
+            if s < 0:
+                lb = lb + (t - 1) * s
+            else:
+                ub = ub + (t - 1) * s
+    else:
+        lb = 0
+        ub = dtype.itemsize
+        for s in ai["shape"]:
+            ub = ub * s
+
+    # grab a flat char array at the sharemem address,
+    # covering the memory region ai refers to.
+    ra = (tp * (ub - lb)).from_address(ai["data"][0] + lb)
+
+    # view it as what it should look like. here we assume numpy
+    # will not do crazy things like modifying gaps due to striding.
+    shm = numpy.ndarray(
+        buffer=ra, dtype=dtype, offset=-lb,
+        strides=ai["strides"], shape=ai["shape"]
+    ).view(type=anonymousmemmap)
+    return shm
+
+
+class anonymousmemmap(numpy.memmap):
+    """Arrays allocated on shared memory.
+
+    The array is stored in an anonymous memory map
+    that is shared between child-processes.
+
+    """
+
+    def __new__(subtype, shape, dtype=numpy.uint8, order="C"):
+
+        descr = numpy.dtype(dtype)
+        _dbytes = descr.itemsize
+
+        shape = numpy.atleast_1d(shape)
+        size = 1
+        for k in shape:
+            size *= k
+
+        bytes = int(size * _dbytes)
+
+        if bytes > 0:
+            mm = mmap.mmap(-1, bytes)
+        else:
+            mm = numpy.empty(0, dtype=descr)
+        self = numpy.ndarray.__new__(
+            subtype, shape, dtype=descr, buffer=mm, order=order
+        )
+        self._mmap = mm
+        return self
+
+    def __array_wrap__(self, outarr, context=None, return_scalar=False):
+        # after ufunc this won't be on shm!
+        if NUMPY_LT_2_0:
+            return numpy.ndarray.__array_wrap__(
+                self.view(numpy.ndarray), outarr, context
+            )
+        else:
+            return numpy.ndarray.__array_wrap__(
+                self.view(numpy.ndarray), outarr, context, return_scalar
+            )
+
+    def __reduce__(self):
+        return __unpickle__, (self.__array_interface__, self.dtype)