Repository: cabouman/svmbir (language: python)

Issue:
Bug fix related to va_arg usage in multialloc
Most importantly, this pulls in a critical fix to the C code related to va_arg usage in multialloc().  On macOS 12.2 (M1) it was observed to produce garbage values for the array size passed to multialloc due to a type mismatch in the va_arg arguments.


Discussion / hints:

Fix patch:
diff --git a/demo.zip b/demo.zip
index e5517cd..8408ea0 100644
Binary files a/demo.zip and b/demo.zip differ
diff --git a/demo/demo_2D_microscopy.py b/demo/demo_2D_microscopy.py
index 8d6db5f..fae37dc 100644
--- a/demo/demo_2D_microscopy.py
+++ b/demo/demo_2D_microscopy.py
@@ -18,11 +18,13 @@
 tilt_angle = np.pi/3 # Tilt range of +-60deg
 
 # Reconstruction parameters
-T = 1.0
-p = 1.2
-sharpness = 0.0
 snr_db = 30.0
-max_resolutions=2
+sharpness = 0.0
+T = 0.1
+p = 1.2
+
+# Multi-resolution works much better for limited and sparse view reconstruction
+max_resolutions=2 # Use 2 additional resolutions to do reconstruction
 
 # Display parameters
 vmin = 0.0
diff --git a/demo/demo_2D_shepp_logan.py b/demo/demo_2D_shepp_logan.py
index 4b54dfe..6d370a9 100644
--- a/demo/demo_2D_shepp_logan.py
+++ b/demo/demo_2D_shepp_logan.py
@@ -16,10 +16,13 @@
 tilt_angle = np.pi/2 # Tilt range of +-90deg
 
 # Reconstruction parameters
-T = 0.1
-p = 1.1
+snr_db = 30.0
 sharpness = 0.0
-snr_db = 40.0
+T = 0.1
+p = 1.2
+
+# Multi-resolution works much better for limited and sparse view reconstruction
+max_resolutions=2 # Use 2 additional resolutions to do reconstruction
 
 # Display parameters
 vmin = 1.0
@@ -39,7 +42,7 @@
 (num_views, num_slices, num_channels) = sino.shape
 
 # Perform MBIR reconstruction
-recon = svmbir.recon(sino, angles, T=T, p=p, sharpness=sharpness, snr_db=snr_db)
+recon = svmbir.recon(sino, angles, T=T, p=p, sharpness=sharpness, snr_db=snr_db, max_resolutions = max_resolutions)
 
 # Compute Normalized Root Mean Squared Error
 nrmse = svmbir.phantom.nrmse(recon[0], phantom[0])
diff --git a/demo/demo_3D_microscopy.py b/demo/demo_3D_microscopy.py
index 6a0ed8b..38d2057 100644
--- a/demo/demo_3D_microscopy.py
+++ b/demo/demo_3D_microscopy.py
@@ -19,14 +19,11 @@
 tilt_angle = np.pi/3 # Tilt range of +-60deg
 
 # Reconstruction parameters
-sharpness = 2.0
-T = 0.25
 snr_db = 30.0
+sharpness = 0.0
+T = 0.25
 p = 1.2
 
-# Multi-resolution works much better for limited and sparse view reconstruction
-max_resolutions=2 # Use 2 additional resolutions to do reconstruction
-
 # Display parameters
 vmin = 0.0
 vmax = 1.1
@@ -44,7 +41,7 @@
 (num_views, num_slices, num_channels) = sino.shape
 
 # Perform MBIR reconstruction
-recon = svmbir.recon(sino, angles, num_rows=num_rows, num_cols=num_cols, max_resolutions=max_resolutions, T=T, p=p, sharpness=sharpness, snr_db=snr_db )
+recon = svmbir.recon(sino, angles, num_rows=num_rows, num_cols=num_cols, T=T, p=p, sharpness=sharpness, snr_db=snr_db )
 
 # Compute Normalized Root Mean Squared Error
 nrmse = svmbir.phantom.nrmse(recon, phantom)
diff --git a/demo/demo_3D_shepp_logan.py b/demo/demo_3D_shepp_logan.py
index a1e0259..280dd03 100644
--- a/demo/demo_3D_shepp_logan.py
+++ b/demo/demo_3D_shepp_logan.py
@@ -18,10 +18,10 @@
 tilt_angle = np.pi/2 # Tilt range of +-90deg
 
 # Reconstruction parameters
-T = 0.1
-p = 1.1
+snr_db = 30.0
 sharpness = 0.0
-snr_db = 40.0
+T = 0.1
+p = 1.2
 
 # Display parameters
 vmin = 1.0
diff --git a/demo/demo_fanbeam.py b/demo/demo_fanbeam.py
index e235076..9dc4ae3 100644
--- a/demo/demo_fanbeam.py
+++ b/demo/demo_fanbeam.py
@@ -8,7 +8,7 @@
 """
 
 # Simulated sinogram parameters
-geometry = 'fan'
+geometry = 'fan-curved'
 dist_source_detector = 1000.0
 delta_channel = 1.0
 magnification = 2.0
@@ -18,10 +18,10 @@
 
 # Reconstruction parameters
 img_size = 256
+snr_db = 30.0
 sharpness = 0.0
-T = 1.0
+T = 0.1
 p = 1.2
-snr_db = 40.0
 
 # Generate phantom with a single slice
 phantom = svmbir.phantom.gen_shepp_logan(img_size,img_size)
@@ -39,12 +39,15 @@
 vmax = 1.2
 plt.figure(); plt.imshow(phantom[0],vmin=vmin,vmax=vmax,cmap='gray'); plt.colorbar()
 plt.title('Shepp Logan Phantom')
+plt.savefig('output/shepp_logan_phantom.png')
 
 plt.figure(); plt.imshow(np.squeeze(sino).T,cmap='gray'); plt.colorbar()
 plt.title('Sinogram')
+plt.savefig('output/shepp_logan_sinogram.png')
 
 plt.figure(); plt.imshow(recon[0],vmin=vmin,vmax=vmax,cmap='gray'); plt.colorbar()
 plt.title(f'Reconstruction, nmrse={nrmse:.3f}')
+plt.savefig('output/shepp_logan_recon_fanbeam.png')
 
 print("Close figures to continue")
 plt.show()
diff --git a/demo/demo_multires_comparison.py b/demo/demo_multires_comparison.py
index f796c8d..12f8cb8 100644
--- a/demo/demo_multires_comparison.py
+++ b/demo/demo_multires_comparison.py
@@ -19,9 +19,9 @@
 tilt_angle = np.pi/3 # Tilt range of +-60deg
 
 # Reconstruction parameters
-sharpness = 2.0
-T = 0.25
 snr_db = 30.0
+sharpness = 0.0
+T = 0.25
 p = 1.2
 
 # Multi-resolution works much better for limited and sparse view reconstruction
@@ -44,7 +44,7 @@
 (num_views, num_slices, num_channels) = sino.shape
 
 # Perform fixed resolution MBIR reconstruction
-recon = svmbir.recon(sino, angles, num_rows=num_rows, num_cols=num_cols, T=T, p=p, sharpness=sharpness, snr_db=snr_db )
+recon = svmbir.recon(sino, angles, num_rows=num_rows, num_cols=num_cols, T=T, p=p, max_resolutions=0, sharpness=sharpness, snr_db=snr_db )
 
 # Perform multi-resolution MBIR reconstruction
 mr_recon = svmbir.recon(sino, angles, num_rows=num_rows, num_cols=num_cols, T=T, p=p, max_resolutions=max_resolutions, sharpness=sharpness, snr_db=snr_db )
@@ -61,10 +61,10 @@
 
 # display fixed resolution reconstruction
 title = f'Slice {display_slice:d} of Fixed Res Recon with NRMSE={nrmse:.3f}.'
-plot_image(recon[display_slice], title=title, filename='output/mr_3D_microscopy_recon.png', vmin=vmin, vmax=vmax)
+plot_image(recon[display_slice], title=title, filename='output/fixed_res_3D_microscopy_recon.png', vmin=vmin, vmax=vmax)
 
 # display multi-resolution reconstruction
 title = f'Slice {display_slice:d} of Multi-Res Recon with NRMSE={mr_nrmse:.3f}.'
-plot_image(recon[display_slice], title=title, filename='output/mr_3D_microscopy_recon.png', vmin=vmin, vmax=vmax)
+plot_image(mr_recon[display_slice], title=title, filename='output/multi_res_3D_microscopy_recon.png', vmin=vmin, vmax=vmax)
 
 input("press Enter")
diff --git a/demo/demo_prox.py b/demo/demo_prox.py
index e6eea09..80e4cb0 100644
--- a/demo/demo_prox.py
+++ b/demo/demo_prox.py
@@ -19,11 +19,8 @@
 tilt_angle = np.pi/2 # Tilt range of +-90deg
 
 # Reconstruction parameters
+snr_db = 30.0
 sigma_p = 0.2
-snr_db = 40.0
-
-# Multi-resolution works much better for limited and sparse view reconstruction
-max_resolutions=1 # Use 2 additional resolutions to do reconstruction
 
 # Display parameters
 vmin = 1.0
@@ -45,7 +42,7 @@
 phantom_rot = np.swapaxes(phantom, 1, 2)
 
 # Perform fixed resolution MBIR reconstruction using proximal map input
-recon = svmbir.recon(sino, angles, max_resolutions=max_resolutions, init_image=phantom_rot, prox_image=phantom_rot, positivity=False, sigma_p=sigma_p, snr_db=snr_db)
+recon = svmbir.recon(sino, angles, init_image=phantom_rot, prox_image=phantom_rot, positivity=False, sigma_p=sigma_p, snr_db=snr_db)
 
 # create output folder
 os.makedirs('output', exist_ok=True)
diff --git a/demo/demo_sino_sorting.py b/demo/demo_sino_sorting.py
index 02e5544..2bf3c9b 100644
--- a/demo/demo_sino_sorting.py
+++ b/demo/demo_sino_sorting.py
@@ -23,7 +23,7 @@
 T = 0.1
 p = 1.1
 sharpness = 0.0
-snr_db = 40.0
+snr_db = 30.0
 
 # Display parameters
 vmin = 1.0
diff --git a/dev_scripts/build_dist.sh b/dev_scripts/build_dist.sh
index 0f7b0a2..8d0939a 100644
--- a/dev_scripts/build_dist.sh
+++ b/dev_scripts/build_dist.sh
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-# Builds the sdist and wheels for python 3.7, 3.8, 3.9
+# Builds the sdist and wheels for python 3.8, 3.9, 3.10
 #
 # RUN AS: source build_dist.sh
 # NOT AS: ./build_dist.sh
@@ -9,7 +9,7 @@
 # NOTES:
 #   * Principally for macOS wheels. For linux, build using 'manylinux' from docker image
 #
-#   * in v0.2.10, numpy dependency is pinned to 1.21.*, which supports Python 3.7-3.9
+#   * starting in v0.3.0, numpy dependency is pinned to 1.22.*, which supports Python 3.8-3.10
 #
 #   * macOS/x86_64: Run build on macOS 10.14 for binaries to be compatibile with macOS>=10.14.
 #     Wheels are delocated to fix a library incompatibility across macOS>=10.14.
@@ -17,8 +17,7 @@
 #   * macOS/arm64 (M1,M2): Limited to python >= 3.8. 'delocation' section will be skipped.
 #
 
-python_versions=("3.7" "3.8" "3.9")
-#python_versions=("3.8" "3.9")      # use this for macOS/arm64
+python_versions=("3.8" "3.9" "3.10")
 CC=gcc
 
 # check for a valid compiler
diff --git a/dev_scripts/manylinux.sh b/dev_scripts/manylinux.sh
index 4833fea..d340246 100755
--- a/dev_scripts/manylinux.sh
+++ b/dev_scripts/manylinux.sh
@@ -30,7 +30,7 @@ function repair_wheel {
 
 # Compile wheels
 #for PYBIN in /opt/python/*/bin; do
-for PYBIN in /opt/python/{cp37,cp38,cp39,pp37,pp38,pp39}*/bin; do
+for PYBIN in /opt/python/{cp38,cp39,cp310,pp38,pp39}*/bin; do
     echo "***"
     echo "*** building for $PYBIN"
     #"${PYBIN}/pip" install -r /io/dev_scripts/dev-requirements.txt
diff --git a/docs/README.rst b/docs/README.rst
index c165e58..f1c8b29 100644
--- a/docs/README.rst
+++ b/docs/README.rst
@@ -12,9 +12,12 @@ Features
 
 System Requirements
 -------------------
-1. GCC compiler version 4.8.5 or above
-2. OpenMP Libraries
-3. Python>=3.6
+* Python 3.8-3.10
+
+Local builds also require,
+
+* GCC compiler version 4.8.5 or above
+* OpenMP Libraries
 
 
 Optional System Requirements
@@ -24,9 +27,8 @@ Fastest reconstruction can be obtained with,
 * Intel-based CPU(s) supporting AVX2,AVX512
 * Intel ICC compiler (included with Intel's free "oneAPI" toolkit)
 
-We also recommend:
+We also recommend installation to a conda virtual environment.
 
-* Installation to a conda virtual environment
 
 License
 -------
diff --git a/docs/requirements.txt b/docs/requirements.txt
index b9714ed..4468af3 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,4 +1,4 @@
-numpy==1.21.*
+numpy==1.22.*
 psutil>=5.8
 Pillow
 Cython
diff --git a/docs/source/install.rst b/docs/source/install.rst
index 9137fa0..93c305d 100644
--- a/docs/source/install.rst
+++ b/docs/source/install.rst
@@ -11,12 +11,12 @@ Installing from Anaconda
 
 1. Create a Virtual Environment (optional, but recommended)
 
-  To install svmbir in an Anaconda virtual environment, run the following::
+  To install to a conda virtual environment, first run the following::
 
     $ conda create --name svmbir python=3.8
     $ conda activate svmbir
 
-2. Add the conda-forge channel with::
+2. If needed, add the conda-forge channel with::
 
     $ conda config --add channels conda-forge
     $ conda config --set channel_priority strict
@@ -34,7 +34,7 @@ Installing from Anaconda
 Installing from PyPI
 --------------------
 
-On most platforms, installing with pip requires a gcc compiler.
+On some platforms, installing with pip will require a gcc compiler.
 Special considerations for gcc installation on a Mac are listed :ref:`below <Mac gcc>`.
 
 1. Create a Virtual Environment (optional, see above)
@@ -48,7 +48,7 @@ Special considerations for gcc installation on a Mac are listed :ref:`below <Mac
     $ pip install svmbir --upgrade
 
   **Note for macOS**:
-  Some older macOS versions may attempt to install from the wheel and fail. If this happens you can
+  Some older mac OS versions may attempt to install from the wheel and fail. If this happens you can
   bypass the wheel and install from source with the command below::
 
     $ pip install --no-binary svmbir svmbir
@@ -92,13 +92,13 @@ Installing from Github source
   The ``svmbir`` installation requires a C compiler with OpenMP libraries for parallel processing.
   The four compilers with reasonable support are GNU ``gcc``, Microsoft Visual C ``msvc``,
   Intel's ``icc``, and the Apple's ``clang`` compiler.
-  The Intel compiler currently produces the best performance on x86 processors,
+  The Intel compiler currently produces the best performance on x86_64 processors,
   however ``gcc`` and ``clang`` are often easier to install.
 
   **Important:** You must first install one of these compilers together with the associated OpenMP libraries on your computer.
   MacOS and Windows users should refer to the instructions :ref:`below <Windows and Mac>` for more details on installation of the compilers, OMP libraries and associated utilities.
 
-  To install svmbir, run one of the following shell commands, depending on the relevant compiler.
+  To install svmbir, run **one of** the following shell commands, depending on the relevant compiler.
   Run this from the root directory of the source code (where setup.py is located)::
 
     $ CC=gcc pip install .
@@ -114,8 +114,8 @@ Installing from Github source
 
 .. _Windows and Mac:
 
-Install notes for MacOS and Windows
---------------------------------------
+Notes for installing on MacOS and Windows from source
+-----------------------------------------------------
 
 Below are some tips for compiling and running the package under the Windows and MacOSx operating systems.
 Linux is more straight forward.
@@ -123,28 +123,23 @@ Linux is more straight forward.
 1. **Intel icc Compiler:**
 The Intel compiler and OMP libraries when coupled with the appropriate Intel x86 processor
 can substantially increase ``svmbir`` performance by enabling the AVX2 instructor set.
-The ``icc/OpenMP`` compiler and libraries exists for Linux, Windows, and MacOS, but may need to be purchased.
-The icc compiler is available `[here] <https://software.intel.com/content/www/us/en/develop/tools/parallel-studio-xe.html>`__.
-
+The ``icc/OpenMP`` compiler and libraries for Linux, Windows, and MacOS are available
+`[from Intel] <https://software.intel.com/content/www/us/en/develop/tools/parallel-studio-xe.html>`__.
 
 2. **Windows Installation:**
-The package will run under Windows, but there tend to be more things that can go wrong due to the wide variety of possible configurations. The following list of recommended configurations have been tested to work, but others are possible:
+The svmbir package can run under Windows OS, but we don't currently have the resources or demand to widely support Windows.
+The following configuration has been known to work:
 
-* *64-bit gcc or Intel icc compiler:* For the command line version, make sure to install a 64bit compiler such as the ``MinGW_64`` available from `[here] <http://winlibs.com>`__ or the Intel ``icc`` compiler as described above. Commonly used gcc compilers are only 32bit and will create ``calloc`` errors when addressing array sizes greater than 2Gb.
+* *64-bit gcc or Intel icc compiler:* Make sure to install a 64-bit compiler such as the ``MinGW_64`` available from `[here] <http://winlibs.com>`__ or the Intel ``icc`` compiler as described above. Commonly used Windows gcc compilers are only 32bit and will create ``calloc`` errors when addressing array sizes greater than 2Gb.
 
-* *MinGW + MSYS environment:* For the command line version, we recommend installing ``MinGW`` including the ``msys`` utilities. These utilities support a minimalist set of traditional UNIX tools.
+* *MinGW + MSYS environment:* We recommend installing ``MinGW`` including the ``msys`` utilities. These utilities support a minimalist set of traditional UNIX tools.
 
 * *Git Bash:* We recommend installing `[Git Bash] <https://gitforwindows.org>`__ to support bash scripting.
 
-One known issue is that in some Windows bash environments the C executable ``mbir_ct.exe`` may not be properly moved to the ``bin`` directory.
-If this occurs, then the problem can be resolved by manually moving the file.
-
 
 .. _Mac gcc:
 
 3. **Installation of gcc on MacOS:**
-If you are installing from PyPI, then we recommend you use the gcc compiler.
-
 Note the default C compiler on macOS, clang, appears as gcc even though it is not gcc.
 To check that you have the correct compiler, run the command ``gcc --version``.
 
@@ -156,13 +151,14 @@ In order to install gcc, you should do the following:
 
 * Use Homebrew to install gcc using the command ``brew install gcc``. This will also install the OMP libraries.
 
-* Create a symbolic link in /usr/local/bin that maps “gcc” to “gcc-10”, or the most current compiler. To do this, do the following command::
+* Create a symbolic link in /usr/local/bin that maps “gcc” to the desired gcc compiler. To do this, execute one of the following depending on the mac version::
 
-    $ ln -sf /usr/local/bin/gcc-10 /usr/local/bin/gcc
+    $ ln -sf /usr/local/bin/gcc-10 /usr/local/bin/gcc       # older macs with x86_64 processors
+    $ ln -sf /opt/homebrew/bin/gcc-11 /usr/local/bin/gcc    # newer macs with M1/M2 arm64 processors
 
-  You can also check the contents of /usr/local/bin to make sure you link to the most current version of gcc that you installed.
+  Check the contents of /usr/local/bin to make sure you link to the most current version of gcc that you installed.
 
-* Finally, check that you are getting the correct compiler by using the command::
+* Finally, check that you are getting the correct compiler using the command::
 
     $ gcc --version
 
@@ -171,11 +167,10 @@ In order to install gcc, you should do the following:
 .. _Mac clang:
 
 4. **Installation of clang on MacOS:**
-Some MacOS users use the ``clang`` compiler provided as part of the Xcode Developer Tools.
-However, this is not recommended for PyPI installation.
-In this case, the ``gcc`` command in the MacOS environment is **not** actually ``gcc``.
-Instead it is an alias to the ``clang`` compiler.
-Therefore, the C code should be compiled using the ``clang`` option.
+It is possible, although not recommended, to build the package using ``clang`` compiler provided with the Xcode Developer Tools.
+In fact Apple appears to include ``gcc`` with Xcode, but closer inspection reveals that ``gcc`` is actually linking
+to ``clang``, which can be verified by running "gcc --version".
+The required compilation flags are different between gcc and clang, so this detail is important.
 
 In order to obtain ``clang`` you will need to install the most up-to-date version of both Xcode
 and ``Command Line Tools for Xcode`` available `[here] <https://developer.apple.com/download/more/>`__.
@@ -192,7 +187,7 @@ The tar file will contain the following files::
 
 These files should be moved to the specified directories.
 You may also need to open the file ``/usr/local/lib/libomp.dylib``.
-This will generate a splash screen that requests permision of OSx to execute the library.
+This will generate a splash screen that requests permission of to execute the library.
 
 In addition, after OS updates, you may need to reinstall the Xcode toolkit using the command::
 
diff --git a/docs/source/pypi.rst b/docs/source/pypi.rst
index 470766b..f3354f5 100644
--- a/docs/source/pypi.rst
+++ b/docs/source/pypi.rst
@@ -2,96 +2,59 @@
 Upload package to PyPI
 ======================
 
-Summary
--------
+This is only relevant for registered maintainers.
 
-The general procedure for releasing a new version to PyPI is,
+Prior to this stage, make sure to complete all the 
+steps for `preparing the release <release.html>`_ .
 
- 1. Complete all the preliminary steps for `preparing the release <release.html>`_
 
- 2. Set up a conda environment with the required tools
+Set up environment and build distribution files
+-----------------------------------------------
 
- 3. Build the binary and source distribution
-
- 4. Upload the release to TestPyPI, and test installation from TestPyPI
-
- 5. Upload the release to PyPI, and test installation from PyPI
-
-There are two files included with the repository that are specific to PyPI:
-MANIFEST.in and pyproject.toml.
-
-
-Set up environment
-------------------
-
-In addition to the standard svmbir requirements, three additional packages are needed
-for the build and upload::
+Run::
 
     $ conda create --name svmbir_pypi python=3.8
     $ conda activate svmbir_pypi
     $ pip install -r requirements.txt
-    $ pip install setuptools twine delocate
-
-``setuptools`` packages Python projects, ``twine`` uploads package to PyPI, and ``delocate`` finds and copys needed dynamic libraries to a directory within a package.
-
-Build distribution files
-------------------------
-
-Run::
-
     $ CC=gcc python setup.py sdist bdist_wheel
 
 Use gcc here for the binary distribution. For now other supported compilers
 will require the user to install the package from source.
 
-For MacOS developers, you should use the oldest MacOS system you have to build
-the package. (Currently, we use MacOS 10.14 to build the package).
-
 
-Delocate wheel files
---------------------
+Delocate wheel files (macOS/x86_64 builds only)
+-----------------------------------------------
 
-``delocate-wheel`` will generate a fixed wheel with dylib files from an existing wheel.
-The commands below generate a fixed wheel, move it to the original wheel location, and clean temporary files::
+This only applies to building wheels for pre-M1 macs.
+``delocate-wheel`` will generate a fixed wheel by including more widely compatible dylib files with the package.
+Run the following command on an older mac version like 10.14 for the widest compatibility::
 
+    $ pip install delocate
     $ cd dist
-    $ delocate-wheel -w fixed_wheels -v svmbir-0.2.3-cp38-cp38-macosx_10_9_x86_64.whl (change version no.)
-    $ mv fixed_wheels/svmbir-0.2.3-cp38-cp38-macosx_10_9_x86_64.whl ./ (change version no.)
-    $ rm -r fixed_wheels
-    $ cd ..
+    $ delocate-wheel -w fixed_wheels -v <svmbir-xxxxx>.whl    # update wheel file name
 
 
 Upload to TestPyPI and test installation
 ----------------------------------------
 
-Before this you'll need to be registered in testpypi, and be granted access as a
-maintainer of the project.
-For authentication, you'll also need to add API tokens from Account Settings.
+This is only available for registered maintainers.
+If granted, you need to be registered in testpypi,
+and for authentication you'll need to add API tokens from Account Settings.
 
- 1. Upload the package to testpypi. NOTE you cannot upload same version more than once::
+ 1. Upload to testpypi. NOTE you cannot upload same version more than once::
 
+    $ pip install twine
     $ python -m twine upload --repository testpypi dist/*
 
  View the package upload here:
- `https://test.pypi.org/project/svmbir/0.2.3/ <https://test.pypi.org/project/svmbir/>`__ (add version no.)
+ `https://test.pypi.org/project/svmbir <https://test.pypi.org/project/svmbir>`__
 
- 2. Create a clean environment and install the package::
+ 2. Test the uploaded package::
 
-    $ conda create --name svmbir_temp python=3.8
-    $ conda activate svmbir_temp
-    $ pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple svmbir==0.2.3  (change version no.)
+    $ pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple svmbir==0.2.10  # change version no.
+    $ python -c "import svmbir"     # spin the wheel
 
- 3. Check the installation::
-
-    $ conda list | grep svmbir
-    $ python -c "import svmbir"
-
- 4. Run one of the `demo scripts <examples.html>`_
-
- 5. Remove temporary environment::
-
-    $ conda deactivate
-    $ conda remove --name svmbir_temp --all
+ 3. Run one of the `demo scripts <examples.html>`_
 
  NOTE: If the install fails and you need to re-test, TEMPORARILY set the version
  number in setup.py from X.X.X to X.X.X.1 (then 2,3,etc.), for further testing.
@@ -102,40 +65,24 @@ For authentication, you'll also need to add API tokens from Account Settings.
 Upload to PyPI and test installation
 ----------------------------------------
 
- Before this you'll need to be registered in pypi, and be granted access as a
- maintainer of the project.
- For authentication, you'll also need to add API tokens from Account Settings.
+This is only available for registered maintainers.
+If granted, you need to be registered in pypi,
+and for authentication you'll need to add API tokens from Account Settings.
 
+ 1. Upload to pypi. NOTE you cannot upload same version more than once::
 
- 1. Activate pypi environment::
-
-    $ conda activate svmbir_pypi
-
- 2. Upload package to pypi. NOTE you cannot upload same version more than once::
-
+    $ pip install twine
     $ python -m twine upload dist/*
 
- View the package upload here: `<https://pypi.org/project/svmbir/>`_
-
- 3. Create a clean environment and install the package::
-
-    $ conda create --name svmbir_temp python=3.8
-    $ conda activate svmbir_temp
-    $ pip install svmbir
-    $ or,
-    $ pip install svmbir==0.2.3 for a specific version number
-
- 4. Check the installation::
-
-    $ conda list | grep svmbir
-    $ python -c "import svmbir"
+ View the package upload here:
+ `https://pypi.org/project/svmbir <https://pypi.org/project/svmbir>`__
 
- 5. Run one of the `demo scripts <examples.html>`_
+ 2. Test the uploaded package::
 
- 6. Remove temporary environment::
+    $ pip install svmbir    # OR, "svmbir==0.2.10" e.g. for a specific version number
+    $ python -c "import svmbir"     # spin the wheel
 
-    $ conda deactivate
-    $ conda remove --name svmbir_temp --all
+ 3. Run one of the `demo scripts <examples.html>`_
 
 
 Reference
diff --git a/docs/source/svmbir.rst b/docs/source/svmbir.rst
index 6f5a2ee..c333ecb 100644
--- a/docs/source/svmbir.rst
+++ b/docs/source/svmbir.rst
@@ -4,15 +4,16 @@ svmbir
    :members: 
    :undoc-members:
    :show-inheritance:
+   :member-order: bysource
 
    .. rubric:: **Functions:**
 
    .. autosummary::
-      auto_sigma_p
-      auto_sigma_x
-      auto_sigma_y
-      backproject
-      calc_weights
-      project
       recon
+      project
+      backproject
       sino_sort
+      calc_weights
+      auto_sigma_x
+      auto_sigma_y
+      auto_sigma_p
diff --git a/pyproject.toml b/pyproject.toml
index 60cd544..e05f2ad 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,3 @@
 [build-system]
-requires = ["setuptools", "wheel", "numpy==1.21.*", "Cython"]
+requires = ["setuptools", "wheel", "numpy==1.22.*", "Cython"]
 build-backend = "setuptools.build_meta"
diff --git a/requirements.txt b/requirements.txt
index 63b322b..c95f35a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-numpy==1.21.*
+numpy==1.22.*
 psutil>=5.8
 Pillow>=9.1
 Cython
diff --git a/setup.py b/setup.py
index e29a12f..17d855e 100644
--- a/setup.py
+++ b/setup.py
@@ -24,7 +24,7 @@
 src_dir = packages_dir + "/sv-mbirct/src/"
 
 # Dependencies for running svmbir. Dependencies for build/installation are in pyproject.toml
-install_requires=['numpy==1.21.*','psutil>=5.8','Pillow']
+install_requires=['numpy==1.22.*','psutil>=5.8','Pillow']
 
 
 # Set up install for Cython or Command line interface
diff --git a/svmbir/__init__.py b/svmbir/__init__.py
index 6e3bb25..734c6f6 100644
--- a/svmbir/__init__.py
+++ b/svmbir/__init__.py
@@ -1,4 +1,4 @@
-__version__ = '0.2.10'
+__version__ = '0.3.0'
 from .svmbir import *
 from .phantom import *
-__all__ = ['_svmbir_lib_path','_clear_cache','sino_sort','auto_sigma_x','auto_sigma_p','auto_sigma_y', 'calc_weights','project','backproject','recon']
+__all__ = ['recon','project','backproject','sino_sort','calc_weights','auto_sigma_x','auto_sigma_y','auto_sigma_p','_clear_cache','_svmbir_lib_path']
diff --git a/svmbir/_utils.py b/svmbir/_utils.py
index 4ee2973..1b19213 100644
--- a/svmbir/_utils.py
+++ b/svmbir/_utils.py
@@ -104,7 +104,7 @@ def test_args_geom(num_rows, num_cols, delta_pixel, roi_radius, delta_channel, c
     return num_rows, num_cols, delta_pixel, roi_radius, delta_channel, center_offset
 
 
-def test_args_recon(sharpness, positivity, max_resolutions, stop_threshold, max_iterations):
+def test_args_recon(sharpness, positivity, relax_factor, max_resolutions, stop_threshold, max_iterations):
 
     sharpness = int_to_float(sharpness)
     if not isinstance(sharpness, float):
@@ -115,9 +115,14 @@ def test_args_recon(sharpness, positivity, max_resolutions, stop_threshold, max_
         warnings.warn("Parameter positivity is not valid boolean; Setting positivity = True.")
         positivity = True
 
-    if not (isinstance(max_resolutions, int) and (max_resolutions >= 0)):
-        warnings.warn("Parameter max_resolutions is not valid int; Setting max_resolutions = 0.")
-        max_resolutions = 0
+    relax_factor = int_to_float(relax_factor)
+    if not isinstance(relax_factor, float):
+        warnings.warn("Parameter relax_factor is not valid float; Setting to 1.0.")
+        relax_factor = 1.0
+
+    if not ((isinstance(max_resolutions, int) and (max_resolutions >= 0)) or (max_resolutions is None)):
+        warnings.warn("Parameter max_resolutions is not valid int; Setting max_resolutions = None.")
+        max_resolutions = None
 
     if isinstance(stop_threshold,int):
         stop_threshold = float(stop_threshold)
@@ -129,7 +134,7 @@ def test_args_recon(sharpness, positivity, max_resolutions, stop_threshold, max_
         warnings.warn("Parameter max_iterations is not valid int; Setting max_iterations = 100.")
         max_iterations = 100
 
-    return sharpness, positivity, max_resolutions, stop_threshold, max_iterations
+    return sharpness, positivity, relax_factor, max_resolutions, stop_threshold, max_iterations
 
 
 def test_args_inits(init_image, prox_image, init_proj, weights, weight_type):
@@ -300,7 +305,7 @@ def get_params_dicts(angles, num_channels, num_views, num_slices, num_rows, num_
 
     return sinoparams, imgparams, settings
 
-def get_reconparams_dicts(sigma_y, positivity, sigma_x, p, q, T, b_interslice,
+def get_reconparams_dicts(sigma_y, positivity, relax_factor, sigma_x, p, q, T, b_interslice,
                             stop_threshold, max_iterations,init_image_value=0, interface = 'Cython'):
     reconparams = dict()
     if interface == 'Command Line':
@@ -319,6 +324,7 @@ def get_reconparams_dicts(sigma_y, positivity, sigma_x, p, q, T, b_interslice,
     reconparams['stop_threshold'] = stop_threshold
     reconparams['max_iterations'] = max_iterations
     reconparams['positivity'] = int(positivity)
+    reconparams['relax_factor'] = relax_factor
 
     if interface == 'Command Line':
         reconparams['weight_type'] = 'unweighted'  # constant weights
diff --git a/svmbir/interface_cy_c.pyx b/svmbir/interface_cy_c.pyx
index 76a3466..2ebc078 100644
--- a/svmbir/interface_cy_c.pyx
+++ b/svmbir/interface_cy_c.pyx
@@ -19,7 +19,7 @@ __namelen_sysmatrix = 20
 cdef extern from "./sv-mbirct/src/MBIRModularDefs.h":
     # 3D Sinogram Parameters
     struct SinoParams3DParallel:
-        int Geometry;           # 0:parallel, 1:fanbeam
+        int Geometry;           # 0:parallel, 1:fanbeam (curved), 2:fanbeam (flat)
         int NChannels;          # Number of channels in detector
         float DeltaChannel;     # Detector spacing
         float CenterOffset;     # Offset of center-of-rotation, computed from center of detector in
@@ -50,6 +50,7 @@ cdef extern from "./sv-mbirct/src/MBIRModularDefs.h":
         float StopThreshold;    # Stopping threshold in percent
         int MaxIterations;      # Maximum number of iterations
         char Positivity;        # Positivity constraint: 1=yes, 0=no
+        float RelaxFactor;      # over/under-relaxation factor, range (0,2.0) [default=1.0]
         # sinogram weighting
         float SigmaY;           # Scaling constant for sinogram weights (e.g. W=exp(-y)/SigmaY^2 )
         int weightType;         # How to compute weights if internal, 1: uniform, 2: exp(-y); 3: exp(-y/2), 4: 1/(y+0.1)
@@ -111,8 +112,10 @@ cdef convert_py2c_SinoParams3D(SinoParams3DParallel* sinoparams,
                         float[:] ViewAngles):
     if py_sinoparams['geometry']=='parallel':
         sinoparams.Geometry = 0
-    elif py_sinoparams['geometry']=='fan':
+    elif py_sinoparams['geometry']=='fan-curved':
         sinoparams.Geometry = 1
+    elif py_sinoparams['geometry']=='fan-flat':
+        sinoparams.Geometry = 2
     else:
         sinoparams.Geometry = 0
     sinoparams.NChannels = py_sinoparams['num_channels']
@@ -135,6 +138,7 @@ cdef convert_py2c_ReconParams3D(ReconParams* reconparams,
     reconparams.StopThreshold = py_reconparams['stop_threshold']     # Stopping threshold in percent
     reconparams.MaxIterations = py_reconparams['max_iterations']     # Maximum number of iterations
     reconparams.Positivity = py_reconparams['positivity']           # Positivity constraint: 1=yes, 0=no
+    reconparams.RelaxFactor = py_reconparams['relax_factor']
     # sinogram weighting
     reconparams.SigmaY = py_reconparams['sigma_y']                   # Scaling constant for sinogram weights (e.g. W=exp(-y)/SigmaY^2 )
     reconparams.weightType = py_reconparams['weight_type']           # How to compute weights if internal, 1: uniform, 2: exp(-y); 3: exp(-y/2), 4: 1/(y+0.1)
@@ -331,7 +335,7 @@ def multires_recon(sino, angles, weights, weight_type, init_image, prox_image, i
                    geometry, dist_source_detector, magnification,
                    num_rows, num_cols, roi_radius, delta_channel, delta_pixel, center_offset,
                    sigma_y, snr_db, sigma_x, p, q, T, b_interslice,
-                   sharpness, positivity, max_resolutions, stop_threshold, max_iterations,
+                   sharpness, positivity, relax_factor, max_resolutions, stop_threshold, max_iterations,
                    num_threads, delete_temps, svmbir_lib_path, object_name, verbose):
     """Multi-resolution SVMBIR reconstruction used by svmbir.recon().
 
@@ -377,7 +381,7 @@ def multires_recon(sino, angles, weights, weight_type, init_image, prox_image, i
                         num_rows=lr_num_rows, num_cols=lr_num_cols, roi_radius=roi_radius,
                         delta_channel=delta_channel, delta_pixel=lr_delta_pixel, center_offset=center_offset,
                         sigma_y=lr_sigma_y, snr_db=snr_db, sigma_x=sigma_x, p=p,q=q,T=T,b_interslice=b_interslice,
-                        sharpness=sharpness, positivity=positivity, max_resolutions=new_max_resolutions,
+                        sharpness=sharpness, positivity=positivity, relax_factor=relax_factor, max_resolutions=new_max_resolutions,
                         stop_threshold=stop_threshold, max_iterations=max_iterations, num_threads=num_threads,
                         delete_temps=delete_temps, svmbir_lib_path=svmbir_lib_path, object_name=object_name,
                         verbose=verbose)
@@ -400,7 +404,7 @@ def multires_recon(sino, angles, weights, weight_type, init_image, prox_image, i
     # Collect parameters to pass to C
     (num_views, num_slices, num_channels) = sino.shape
 
-    reconparams = utils.get_reconparams_dicts(sigma_y, positivity, sigma_x, p, q, T, b_interslice,
+    reconparams = utils.get_reconparams_dicts(sigma_y, positivity, relax_factor, sigma_x, p, q, T, b_interslice,
                         stop_threshold, max_iterations, interface = 'Cython')
 
     paths, sinoparams, imgparams = _init_geometry(angles, center_offset=center_offset,
diff --git a/svmbir/interface_py_c.py b/svmbir/interface_py_c.py
index 9433dd4..9f182de 100644
--- a/svmbir/interface_py_c.py
+++ b/svmbir/interface_py_c.py
@@ -35,6 +35,7 @@
                          'stop_threshold' : 'StopThreshold',
                          'max_iterations' : 'MaxIterations',
                          'positivity' : 'Positivity',
+                         'relax_factor' : 'RelaxFactor',
                          'weight_type' : 'weightType',
                          'geometry' : 'Geometry',
                          'dist_source_detector' : 'DistSourceDetector',
@@ -185,7 +186,7 @@ def multires_recon(sino, angles, weights, weight_type, init_image, prox_image, i
                    geometry, dist_source_detector, magnification,
                    num_rows, num_cols, roi_radius, delta_channel, delta_pixel, center_offset,
                    sigma_y, snr_db, sigma_x, p, q, T, b_interslice,
-                   sharpness, positivity, max_resolutions, stop_threshold, max_iterations,
+                   sharpness, positivity, relax_factor, max_resolutions, stop_threshold, max_iterations,
                    num_threads, delete_temps, svmbir_lib_path, object_name, verbose):
     """Multi-resolution SVMBIR reconstruction used by svmbir.recon().
 
@@ -228,7 +229,7 @@ def multires_recon(sino, angles, weights, weight_type, init_image, prox_image, i
                         num_rows=lr_num_rows, num_cols=lr_num_cols, roi_radius=roi_radius,
                         delta_channel=delta_channel, delta_pixel=lr_delta_pixel, center_offset=center_offset,
                         sigma_y=lr_sigma_y, snr_db=snr_db, sigma_x=sigma_x, p=p,q=q,T=T,b_interslice=b_interslice,
-                        sharpness=sharpness, positivity=positivity, max_resolutions=new_max_resolutions,
+                        sharpness=sharpness, positivity=positivity, relax_factor=relax_factor, max_resolutions=new_max_resolutions,
                         stop_threshold=stop_threshold, max_iterations=max_iterations, num_threads=num_threads,
                         delete_temps=delete_temps, svmbir_lib_path=svmbir_lib_path, object_name=object_name,
                         verbose=verbose)
@@ -249,7 +250,7 @@ def multires_recon(sino, angles, weights, weight_type, init_image, prox_image, i
     else :
         init_image_value = 0
 
-    reconparams = utils.get_reconparams_dicts(sigma_y, positivity, sigma_x, p, q, T, b_interslice,
+    reconparams = utils.get_reconparams_dicts(sigma_y, positivity, relax_factor, sigma_x, p, q, T, b_interslice,
                             stop_threshold, max_iterations,init_image_value=init_image_value, interface = 'Command Line')
 
     paths, sinoparams, imgparams = _init_geometry(angles, center_offset=center_offset,
diff --git a/svmbir/sv-mbirct b/svmbir/sv-mbirct
index e02e064..2220c0e 160000
--- a/svmbir/sv-mbirct
+++ b/svmbir/sv-mbirct
@@ -1,1 +1,1 @@
-Subproject commit e02e064aa1029434260fdccf6f899dcc78b6b5c9
+Subproject commit 2220c0e04232c18aa963f6cded1d9ab7a4df1af0
diff --git a/svmbir/svmbir.py b/svmbir/svmbir.py
index 8459659..77c2212 100644
--- a/svmbir/svmbir.py
+++ b/svmbir/svmbir.py
@@ -6,6 +6,8 @@
 import shutil
 import numpy as np
 import os
+import sys
+import warnings
 import svmbir._utils as utils
 
 if os.environ.get('CLIB') =='CMD_LINE':
@@ -15,6 +17,7 @@
 
 __svmbir_lib_path = os.path.join(os.path.expanduser('~'), '.cache', 'svmbir')
 
+
 def _svmbir_lib_path():
     """Returns the path to the cache directory used by svmbir
     """
@@ -100,7 +103,24 @@ def calc_weights(sino, weight_type ):
     return weights
 
 
-def auto_sigma_y(sino, weights, snr_db = 30.0, delta_pixel = 1.0, delta_channel = 1.0):
+def auto_max_resolutions(init_image) :
+    """Compute the automatic value of ``max_resolutions`` for use in MBIR reconstruction.
+
+    Args:
+        init_image (ndarray): Initial image for reconstruction.
+    Returns:
+        int: Automatic value of ``max_resolutions``.
+    """
+    # Default value of max_resolutions
+    max_resolutions = 2
+    if isinstance(init_image, np.ndarray) and (init_image.ndim == 3):
+        #print('Init image present. Setting max_resolutions = 0.')
+        max_resolutions = 0
+
+    return max_resolutions
+
+
+def auto_sigma_y(sino, weights, magnification = 1.0, delta_channel = 1.0, delta_pixel = 1.0, snr_db = 30.0 ) :
     """Compute the automatic value of ``sigma_y`` for use in MBIR reconstruction.
 
     Args:
@@ -109,12 +129,14 @@ def auto_sigma_y(sino, weights, snr_db = 30.0, delta_pixel = 1.0, delta_channel
         weights (ndarray):
             3D numpy array of weights with same shape as sino.
             The parameters weights should be the same values as used in svmbir reconstruction.
-        snr_db (float, optional):
-            [Default=30.0] Scalar value that controls assumed signal-to-noise ratio of the data in dB.
-        delta_pixel (float, optional):
-            [Default=1.0] Scalar value of pixel spacing in :math:`ALU`.
+        magnification (float):
+            (fan beam geometries only) Magnification factor = dist_source_detector/dist_source_isocenter.
         delta_channel (float, optional):
             [Default=1.0] Scalar value of detector channel spacing in :math:`ALU`.
+        delta_pixel (float, optional):
+            [Default=1.0] Scalar value of pixel spacing in :math:`ALU`.
+        snr_db (float, optional):
+            [Default=30.0] Scalar value that controls assumed signal-to-noise ratio of the data in dB.
 
 
     Returns:
@@ -129,8 +151,14 @@ def auto_sigma_y(sino, weights, snr_db = 30.0, delta_pixel = 1.0, delta_channel
     # convert snr to relative noise standard deviation
     rel_noise_std = 10 ** (-snr_db / 20)
 
+    # compute the default_pixel_pitch = the detector pixel pitch in the image plane given the magnification
+    default_pixel_pitch = delta_channel / magnification
+
+    # compute the image pixel pitch relative to the default.
+    pixel_pitch_relative_to_default = delta_pixel / default_pixel_pitch
+
     # compute sigma_y and scale by relative pixel and detector pitch
-    sigma_y = rel_noise_std * signal_rms * (delta_pixel / delta_channel) ** (0.5)
+    sigma_y = rel_noise_std * signal_rms * pixel_pitch_relative_to_default ** (0.5)
 
     if sigma_y > 0:
         return sigma_y
@@ -138,12 +166,14 @@ def auto_sigma_y(sino, weights, snr_db = 30.0, delta_pixel = 1.0, delta_channel
         return 1.0
 
 
-def auto_sigma_x(sino, delta_channel = 1.0, sharpness = 0.0 ):
+def auto_sigma_x(sino, magnification = 1.0, delta_channel = 1.0, sharpness = 0.0 ):
     """Compute the automatic value of ``sigma_x`` for use in MBIR reconstruction.
 
     Args:
         sino (ndarray):
             3D numpy array of sinogram data with shape (num_views,num_slices,num_channels).
+        magnification (float):
+            (fan beam geometries only) Magnification factor = dist_source_detector/dist_source_isocenter.
         delta_channel (float, optional):
             [Default=1.0] Scalar value of detector channel spacing in :math:`ALU`.
         sharpness (float, optional):
@@ -153,15 +183,17 @@ def auto_sigma_x(sino, delta_channel = 1.0, sharpness = 0.0 ):
     Returns:
         float: Automatic value of regularization parameter.
     """
-    return 0.2 * auto_sigma_prior(sino, delta_channel, sharpness)
+    return 0.2 * auto_sigma_prior(sino, magnification, delta_channel, sharpness)
 
 
-def auto_sigma_p(sino, delta_channel = 1.0, sharpness = 0.0 ):
+def auto_sigma_p(sino, magnification = 1.0, delta_channel = 1.0, sharpness = 0.0 ):
     """Compute the automatic value of ``sigma_p`` for use in proximal map estimation.
 
     Args:
         sino (ndarray):
             3D numpy array of sinogram data with shape (num_views,num_slices,num_channels).
+        magnification (float):
+            (fan beam geometries only) Magnification factor = dist_source_detector/dist_source_isocenter.
         delta_channel (float, optional):
             [Default=1.0] Scalar value of detector channel spacing in :math:`ALU`.
         sharpness (float, optional):
@@ -171,15 +203,17 @@ def auto_sigma_p(sino, delta_channel = 1.0, sharpness = 0.0 ):
     Returns:
         float: Automatic value of regularization parameter.
     """
-    return 1.0 * auto_sigma_prior(sino, delta_channel, sharpness)
+    return 1.0 * auto_sigma_prior(sino, magnification, delta_channel, sharpness)
 
 
-def auto_sigma_prior(sino, delta_channel = 1.0, sharpness = 0.0 ):
+def auto_sigma_prior(sino, magnification = 1.0, delta_channel = 1.0, sharpness = 0.0 ):
     """Compute the automatic value of prior model regularization term for use in MBIR reconstruction or proximal map estimation. This subroutine is called by ``auto_sigma_x`` in MBIR reconstruction, or ``auto_sigma_p`` in proximal map estimation.
 
     Args:
         sino (ndarray):
             3D numpy array of sinogram data with shape (num_views,num_slices,num_channels).
+        magnification (float):
+            (fan beam geometries only) Magnification factor = dist_source_detector/dist_source_isocenter.
         delta_channel (float, optional):
             [Default=1.0] Scalar value of detector channel spacing in :math:`ALU`.
         sharpness (float, optional):
@@ -195,7 +229,7 @@ def auto_sigma_prior(sino, delta_channel = 1.0, sharpness = 0.0 ):
     sino_indicator = _sino_indicator(sino)
 
     # Compute a typical image value by dividing average sinogram value by a typical projection path length
-    typical_img_value = np.average(sino, weights=sino_indicator) / (num_channels * delta_channel)
+    typical_img_value = np.average(sino, weights=sino_indicator) / (num_channels * delta_channel / magnification)
 
     # Compute sigma_p as the typical image value when sharpness==0
     sigma_prior = (2 ** sharpness) * typical_img_value
@@ -203,16 +237,9 @@ def auto_sigma_prior(sino, delta_channel = 1.0, sharpness = 0.0 ):
     return sigma_prior
 
 
-def auto_img_size(geometry, num_channels, delta_channel, delta_pixel, magnification=1.0):
+def auto_img_size(num_channels, delta_channel, delta_pixel, magnification=1.0):
     "Compute the default image size"
-
-    if geometry == 'parallel':
-        num_rows = int(np.ceil(num_channels * delta_channel / delta_pixel))
-    elif geometry == 'fan':
-        num_rows = int(np.ceil(num_channels * delta_channel / (delta_pixel*magnification) ))
-    else:
-        raise Exception('Unrecognized geometry {}'.format(geometry))
-
+    num_rows = int(np.ceil(num_channels * delta_channel / (delta_pixel*magnification) ))
     num_cols = num_rows
     return num_rows,num_cols
 
@@ -259,7 +286,7 @@ def recon(sino, angles,
           num_rows = None, num_cols = None, roi_radius = None,
           delta_channel = 1.0, delta_pixel = None, center_offset = 0.0,
           sigma_y = None, snr_db = 30.0, sigma_x = None, sigma_p = None, p = 1.2, q = 2.0, T = 1.0, b_interslice = 1.0,
-          sharpness = 0.0, positivity = True, max_resolutions = 0, stop_threshold = 0.02, max_iterations = 100,
+          sharpness = 0.0, positivity = True, relax_factor=1.0, max_resolutions = None, stop_threshold = 0.02, max_iterations = 100,
           num_threads = None, delete_temps = True, svmbir_lib_path = __svmbir_lib_path, object_name = 'object',
           verbose = 1) :
     """recon(sino, angles, geometry = 'parallel', **kwargs)
@@ -270,8 +297,12 @@ def recon(sino, angles,
         sino (ndarray): 3D sinogram array with shape (num_views, num_slices, num_channels).
         angles (ndarray): 1D view angles array in radians.
         geometry (string):
-            [Default='parallel'] Scanner geometry, either 'parallel' or 'fan'. Note for fan geometry
+            [Default='parallel'] Scanner geometry: 'parallel', 'fan-curved', or 'fan-flat'. Note for fan geometries
             the ``dist_source_detector`` and ``magnification`` arguments must be specified.
+        dist_source_detector (float):
+            (Required for fan beam geometries only) Distance from X-ray focal spot to detectors, in :math:`ALU`.
+        magnification (float):
+            (Required for fan beam geometries only) Magnification factor = dist_source_detector/dist_source_isocenter.
         weights (ndarray, optional): [Default=None] 3D weights array with same shape as sino.
         weight_type (string, optional): [Default="unweighted"] Type of noise model used for data.
             If the ``weights`` array is not supplied, then the function ``svmbir.calc_weights`` is 
@@ -294,17 +325,13 @@ def recon(sino, angles,
             If None, automatically set with auto_roi_radius().
             Pixels outside the radius roi_radius in the :math:`(x,y)` plane are disregarded in the reconstruction.
         delta_channel (float, optional): [Default=1.0] Scalar value of detector channel spacing in :math:`ALU`.
-        delta_pixel (float, optional): Scalar value of the spacing between image pixels in the 2D slice 
-            plane in :math:`ALU`. Defaults to ``delta_channel`` for ``parallel`` beam geometry, 
-            and ``delta_channel``/``magnification`` for ``fan`` geometry.
+        delta_pixel (float, optional): Scalar value of the spacing between image pixels in the 2D slice
+            plane in :math:`ALU`. Defaults to ``delta_channel`` for ``parallel`` beam geometry,
+            and ``delta_channel``/``magnification`` for fan beam geometries.
         center_offset (float, optional): [Default=0.0] Scalar value of offset from center-of-rotation.
-        dist_source_detector (float):
-            (Required, 'fan' geometry only) Distance from X-ray focal spot to detectors, in :math:`ALU`.
-        magnification (float):
-            (Required, 'fan' geometry only) Magnification factor = dist_source_detector/dist_source_isocenter.
         sigma_y (float, optional): [Default=None] Scalar value of noise standard deviation parameter.
             If None, automatically set with auto_sigma_y.
-        snr_db (float, optional): [Default=30.0] Scalar value that controls assumed signal-to-noise 
+        snr_db (float, optional): [Default=30.0] Scalar value that controls assumed signal-to-noise
             ratio of the data in dB. Ignored if sigma_y is not None.
         sigma_x (float, optional): [Default=None] Scalar value :math:`>0` that specifies the qGGMRF scale parameter.
             Ignored if prox_image is not None.
@@ -314,7 +341,7 @@ def recon(sino, angles,
             If None, automatically set with auto_sigma_p. Regularization should be controled with the 
             ``sharpness`` parameter, but ``sigma_p`` can be set directly by expert users.
         p (float, optional): [Default=1.2] Scalar value in range :math:`[1,2]` that specifies the qGGMRF shape parameter.
-        q (float, optional): [Default=2.0] Scalar value in range :math:`[p,1]` that specifies the qGGMRF shape parameter.
+        q (float, optional): [Default=2.0] Scalar value in range :math:`[p,2]` that specifies the qGGMRF shape parameter.
         T (float, optional): [Default=1.0] Scalar value :math:`>0` that specifies the qGGMRF threshold parameter.
         b_interslice (float, optional): [Default=1.0] Scalar value :math:`>0` that specifies the interslice regularization.
             The default value of 1.0 should be fine for most applications.
@@ -327,8 +354,11 @@ def recon(sino, angles,
         positivity (bool, optional): [Default=True] Boolean value that determines if positivity constraint 
             is enforced. The positivity parameter defaults to True; however, it should be changed to False 
             when used in applications that can generate negative image values.
-        max_resolutions (int, optional): [Default=0] Integer >=0 that specifies the maximum number of grid 
+        relax_factor (float, optional): [Default=1.0] Relaxation factor for pixel update. Valid range (0,2.0].
+            Values in (0,1) produce under-relaxation (smaller step size); Values in (1,2] produce over-relaxation.
+        max_resolutions (int, optional): [Default=None] Integer >=0 that specifies the maximum number of grid
             resolutions used to solve MBIR reconstruction problem.
+            If None, automatically set with auto_max_resolutions to 0 if inital image is provided and 2 otherwise.
         stop_threshold (float, optional): [Default=0.02] Scalar valued stopping threshold in percent.
             If stop_threshold=0.0, then run max iterations.
         max_iterations (int, optional): [Default=100] Integer valued specifying the maximum number of 
@@ -349,19 +379,38 @@ def recon(sino, angles,
         3D numpy array: 3D reconstruction with shape (num_slices,num_rows,num_cols) in units of :math:`ALU^{-1}`.
     """
 
+    # Issue notice of change of default regularization for 1 or 2 release cycles
+    #if snr_db is None:
+    #    if new_reg_defaults is True:
+    #        snr_db = 40.0
+    #    else:
+    #        snr_db = 30.0
+    #        sys.stderr.write("SVMBIR v0.3.0 NOTICE of pending change in regularization:\n")
+    #        sys.stderr.write("** Default image regularization and effect of 'sharpness' will change in the\n")
+    #        sys.stderr.write("** next release. To apply the changes immediately supply the following argument:\n")
+    #        sys.stderr.write("** svmbir.recon(...,new_reg_defaults=True)\n")
+
+    # Issue warning that 'fan' geometry will be removed in the future (last valid version is v0.2.9)
+    if geometry=='fan':
+        geometry = 'fan-curved'
+        warnings.warn("'fan' geometry will be removed in a future release. Fan beam geometry is now specified as either 'fan-curved' or 'fan-flat'. Defaulting to 'fan-curved'.",FutureWarning)
+
     # If not specified, then set number of threads = to number of processors
     if num_threads is None :
         num_threads = cpu_count(logical=False)
 
     # Test for valid sino and angles structure. If sino is 2D, make it 3D
     angles = utils.test_args_angles(angles)
-    sino = utils.test_args_sino(sino,angles)
+    sino = utils.test_args_sino(sino, angles)
     (num_views, num_slices, num_channels) = sino.shape
 
     # Tests parameters for valid types and values; print warnings if necessary; and return default values.
-    num_rows, num_cols, delta_pixel, roi_radius, delta_channel, center_offset = utils.test_args_geom(num_rows, num_cols, delta_pixel, roi_radius, delta_channel, center_offset)
-    sharpness, positivity, max_resolutions, stop_threshold, max_iterations = utils.test_args_recon(sharpness, positivity, max_resolutions, stop_threshold, max_iterations)
-    init_image, prox_image, init_proj, weights, weight_type = utils.test_args_inits(init_image, prox_image, init_proj, weights, weight_type)
+    num_rows, num_cols, delta_pixel, roi_radius, delta_channel, center_offset = utils.test_args_geom(
+        num_rows, num_cols, delta_pixel, roi_radius, delta_channel, center_offset)
+    sharpness, positivity, relax_factor, max_resolutions, stop_threshold, max_iterations = utils.test_args_recon(
+        sharpness, positivity, relax_factor, max_resolutions, stop_threshold, max_iterations)
+    init_image, prox_image, init_proj, weights, weight_type = utils.test_args_inits(
+        init_image, prox_image, init_proj, weights, weight_type)
     sigma_y, snr_db, sigma_x, sigma_p = utils.test_args_noise(sigma_y, snr_db, sigma_x, sigma_p)
     p, q, T, b_interslice = utils.test_args_qggmrf(p, q, T, b_interslice)
     num_threads, delete_temps, verbose = utils.test_args_sys(num_threads, delete_temps, verbose)
@@ -370,19 +419,23 @@ def recon(sino, angles,
     if geometry == 'parallel':
         dist_source_detector = 0.0
         magnification = 1.0
-    elif geometry == 'fan':
+    elif geometry=='fan-curved' or geometry=='fan-flat':
         if dist_source_detector is None or magnification is None:
-            raise Exception('For fanbeam geometry, need to specify dist_source_detector and magnification')
+            raise Exception('For fan beam geometries, need to specify dist_source_detector and magnification')
     else:
         raise Exception('Unrecognized geometry {}'.format(geometry))
 
+    # Set automatic value of max_resolutions
+    if max_resolutions is None :
+        max_resolutions = auto_max_resolutions(init_image)
+
     # Set automatic values of num_rows, num_cols, and roi_radius
     if delta_pixel is None:
         delta_pixel = delta_channel/magnification
     if num_rows is None:
-        num_rows,_ = auto_img_size(geometry, num_channels, delta_channel, delta_pixel, magnification)
+        num_rows,_ = auto_img_size(num_channels, delta_channel, delta_pixel, magnification)
     if num_cols is None:
-        _,num_cols = auto_img_size(geometry, num_channels, delta_channel, delta_pixel, magnification)
+        _,num_cols = auto_img_size(num_channels, delta_channel, delta_pixel, magnification)
     if roi_radius is None:
         roi_radius = auto_roi_radius(delta_pixel, num_rows, num_cols)
 
@@ -392,22 +445,22 @@ def recon(sino, angles,
 
     # Set automatic value of sigma_y
     if sigma_y is None:
-        sigma_y = auto_sigma_y(sino, weights, snr_db, delta_pixel=delta_pixel, delta_channel=delta_channel)
+        sigma_y = auto_sigma_y(sino, weights, magnification, delta_channel=delta_channel, delta_pixel=delta_pixel, snr_db=snr_db)
 
     # Set automatic value of sigma_x
     # if qGGMRF mode, then set sigma_x either using the provided value by user, or with auto_sigma_x
     if prox_image is None:
         if sigma_x is None:
-            sigma_x = auto_sigma_x(sino, delta_channel, sharpness)
+            sigma_x = auto_sigma_x(sino, magnification, delta_channel, sharpness)
     # if proximal map mode, then overwrite sigma_x with sigma_p
     else:
         if sigma_p is None:
-            sigma_p = auto_sigma_p(sino, delta_channel, sharpness)
+            sigma_p = auto_sigma_p(sino, magnification, delta_channel, sharpness)
         sigma_x = sigma_p
 
     # Reduce num_threads for positivity=False if problems size calls for it
-    #num_threads_max = max_threads(num_threads, num_slices, num_rows, num_cols, positivity=positivity)
-    #if num_threads_max < num_threads:
+    # num_threads_max = max_threads(num_threads, num_slices, num_rows, num_cols, positivity=positivity)
+    # if num_threads_max < num_threads:
     #    num_threads = num_threads_max
     os.environ['OMP_NUM_THREADS'] = str(num_threads)
     os.environ['OMP_DYNAMIC'] = 'true'
@@ -418,7 +471,7 @@ def recon(sino, angles,
                                        num_rows=num_rows, num_cols=num_cols, roi_radius=roi_radius,
                                        delta_channel=delta_channel, delta_pixel=delta_pixel, center_offset=center_offset,
                                        sigma_y=sigma_y, snr_db=snr_db, sigma_x=sigma_x, p=p, q=q, T=T, b_interslice=b_interslice,
-                                       sharpness=sharpness, positivity=positivity, max_resolutions=max_resolutions,
+                                       sharpness=sharpness, positivity=positivity, relax_factor=relax_factor, max_resolutions=max_resolutions,
                                        stop_threshold=stop_threshold, max_iterations=max_iterations, num_threads=num_threads,
                                        delete_temps=delete_temps, svmbir_lib_path=svmbir_lib_path, object_name=object_name,
                                        verbose=verbose)
@@ -427,9 +480,9 @@ def recon(sino, angles,
 
 
 
-def project(image, angles, num_channels, geometry = 'parallel',
+def project(image, angles, num_channels,
+            geometry = 'parallel', dist_source_detector = None, magnification = None,
             delta_channel = 1.0, delta_pixel = None, center_offset = 0.0, roi_radius = None,
-            dist_source_detector = None, magnification = None,
             num_threads = None, svmbir_lib_path = __svmbir_lib_path, delete_temps = True,
             object_name = 'object', verbose = 1):
     """project(image, angles, num_channels, geometry = 'parallel', **kwargs)
@@ -447,21 +500,21 @@ def project(image, angles, num_channels, geometry = 'parallel',
         num_channels (int):
             Number of sinogram channels.
         geometry (string):
-            [Default='parallel'] Scanner geometry, either 'parallel' or 'fan'. Note for fan geometry
+            [Default='parallel'] Scanner geometry: 'parallel', 'fan-curved', or 'fan-flat'. Note for fan geometries
             the ``dist_source_detector`` and ``magnification`` arguments must be specified.
+        dist_source_detector (float):
+            (Required for fan beam geometries only) Distance from X-ray focal spot to detectors, in :math:`ALU`.
+        magnification (float):
+            (Required for fan beam geometries only) Magnification factor = dist_source_detector/dist_source_isocenter.
         delta_channel (float, optional): [Default=1.0] Scalar value of detector channel spacing in :math:`ALU`.
         delta_pixel (float, optional): Scalar value of the spacing between image pixels in the 2D slice 
             plane in :math:`ALU`. Defaults to ``delta_channel`` for ``parallel`` beam geometry, 
-            and ``delta_channel``/``magnification`` for ``fan`` geometry.
+            and ``delta_channel``/``magnification`` for fan beam geometries.
         center_offset (float, optional):
             [Default=0.0] Offset from center-of-rotation in 'fractional number of channels' units.
         roi_radius (float, optional): [Default=None] Radius of relevant image region in :math:`ALU`.
             Pixels outside the radius are disregarded in the forward projection.
             If not given, the value is set with auto_roi_radius().
-        dist_source_detector (float):
-            (Required, 'fan' geometry only) Distance from X-ray focal spot to detectors, in :math:`ALU`.
-        magnification (float):
-            (Required, 'fan' geometry only) Magnification factor = dist_source_detector/dist_source_isocenter.
         num_threads (int, optional): [Default=None] Number of compute threads requested when executed.
             If None, num_threads is set to the number of cores in the system.
         svmbir_lib_path (string, optional):
@@ -500,13 +553,18 @@ def project(image, angles, num_channels, geometry = 'parallel',
     num_cols = image.shape[2]
     num_views = len(angles)
 
+    # Issue warning that 'fan' geometry will be removed in the future (last valid version is v0.2.9)
+    if geometry=='fan':
+        geometry = 'fan-curved'
+        warnings.warn("'fan' geometry will be removed in a future release. Fan beam geometry is now specified as either 'fan-curved' or 'fan-flat'. Defaulting to 'fan-curved'.",FutureWarning)
+
     # Geometry dependent settings
     if geometry == 'parallel':
         dist_source_detector = 0.0
         magnification = 1.0
-    elif geometry == 'fan':
+    elif geometry=='fan-curved' or geometry=='fan-flat':
         if dist_source_detector is None or magnification is None:
-            raise Exception('For fanbeam geometry, need to specify dist_source_detector and magnification')
+            raise Exception('For fan beam geometries, need to specify dist_source_detector and magnification')
     else:
         raise Exception('Unrecognized geometry {}'.format(geometry))
 
@@ -540,13 +598,12 @@ def project(image, angles, num_channels, geometry = 'parallel',
     return proj
 
 
-
-def backproject(sino, angles, geometry = 'parallel', num_rows=None, num_cols=None,
-            dist_source_detector = None, magnification = None,
+def backproject(sino, angles, num_rows=None, num_cols=None,
+            geometry = 'parallel', dist_source_detector = None, magnification = None,
             delta_channel = 1.0, delta_pixel = None, center_offset = 0.0, roi_radius = None,
             num_threads = None, svmbir_lib_path = __svmbir_lib_path, delete_temps = True,
             object_name = 'object', verbose = 1):
-    """backproject(sino, angles, geometry = 'parallel', **kwargs)
+    """backproject(sino, angles, **kwargs)
 
     Compute 3D back-projection.
 
@@ -556,23 +613,22 @@ def backproject(sino, angles, geometry = 'parallel', num_rows=None, num_cols=Non
         angles (ndarray):
             1D numpy array of view angles in radians.
             'angles[k]' is the angle in radians for view :math:`k`.
-        geometry (string):
-            [Default='parallel'] Scanner geometry, either 'parallel' or 'fan'. Note for fan geometry
-            the ``dist_source_detector`` and ``magnification`` arguments must be specified.
         num_rows (int, optional):
             [Default=num_channels] Integer number of output image rows.
         num_cols (int, optional):
             [Default=num_channels] Integer number of output image columns.
+        geometry (string):
+            [Default='parallel'] Scanner geometry: 'parallel', 'fan-curved', or 'fan-flat'. Note for fan geometries
+            the ``dist_source_detector`` and ``magnification`` arguments must be specified.
         dist_source_detector (float):
-            (Required, 'fan' geometry only) Distance from X-ray focal spot to detectors, in :math:`ALU`.
+            (Required for fan beam geometries only) Distance from X-ray focal spot to detectors, in :math:`ALU`.
         magnification (float):
-            (Required, 'fan' geometry only) Magnification factor = dist_source_detector/dist_source_isocenter.
+            (Required for fan beam geometries only) Magnification factor = dist_source_detector/dist_source_isocenter.
         delta_channel (float, optional):
             [Default=1.0] Detector channel spacing in :math:`ALU`.
-        delta_pixel (float, optional):
-            Size of image pixels in the 2D slice plane in :math:`ALU`.
-            Defaults to ``delta_channel`` for ``parallel`` beam geometry, 
-            and ``delta_channel``/``magnification`` for ``fan`` geometry.
+        delta_pixel (float, optional): Scalar value of the spacing between image pixels in the 2D slice 
+            plane in :math:`ALU`. Defaults to ``delta_channel`` for ``parallel`` beam geometry, 
+            and ``delta_channel``/``magnification`` for fan beam geometries.
         center_offset (float, optional):
             [Default=0.0] Offset from center-of-rotation in 'fractional number of channels' units.
         roi_radius (float, optional): [Default=None] Radius of relevant image region in :math:`ALU`.
@@ -609,22 +665,27 @@ def backproject(sino, angles, geometry = 'parallel', num_rows=None, num_cols=Non
     if num_views != len(angles):
         raise Exception('svmbir.backproject(): angles and sinogram arrays have conflicting sizes')
 
+    # Issue warning that 'fan' geometry will be removed in the future (last valid version is v0.2.9)
+    if geometry=='fan':
+        geometry = 'fan-curved'
+        warnings.warn("'fan' geometry will be removed in a future release. Fan beam geometry is now specified as either 'fan-curved' or 'fan-flat'. Defaulting to 'fan-curved'.",FutureWarning)
+
     # Geometry dependent settings
     if geometry == 'parallel':
         dist_source_detector = 0.0
         magnification = 1.0
-    elif geometry == 'fan':
+    elif geometry=='fan-curved' or geometry=='fan-flat':
         if dist_source_detector is None or magnification is None:
-            raise Exception('For fanbeam geometry, need to specify dist_source_detector and magnification')
+            raise Exception('For fan beam geometries, need to specify dist_source_detector and magnification')
     else:
         raise Exception('Unrecognized geometry {}'.format(geometry))
 
     if delta_pixel is None:
         delta_pixel = delta_channel/magnification
     if num_rows is None:
-        num_rows,_ = auto_img_size(geometry, num_channels, delta_channel, delta_pixel, magnification)
+        num_rows,_ = auto_img_size(num_channels, delta_channel, delta_pixel, magnification)
     if num_cols is None:
-        _,num_cols = auto_img_size(geometry, num_channels, delta_channel, delta_pixel, magnification)
+        _,num_cols = auto_img_size(num_channels, delta_channel, delta_pixel, magnification)
     if roi_radius is None:
         roi_radius = auto_roi_radius(delta_pixel, num_rows, num_cols)
 


Test patch:
diff --git a/dev_scripts/test_pypi.sh b/dev_scripts/test_pypi.sh
index 6e86134..403bae6 100644
--- a/dev_scripts/test_pypi.sh
+++ b/dev_scripts/test_pypi.sh
@@ -8,8 +8,8 @@
 #
 # Set these accordingly:
 
-python_versions=("3.7" "3.8" "3.9")
-svmbir_version=0.2.10
+python_versions=("3.8" "3.9" "3.10")
+svmbir_version=0.3.0
 
 echo "*********************************************************"
 echo "**** Test install "
diff --git a/svmbir/tests/test_recon.py b/svmbir/tests/test_recon.py
index 1911313..3843089 100644
--- a/svmbir/tests/test_recon.py
+++ b/svmbir/tests/test_recon.py
@@ -31,7 +31,7 @@ def test_3d_shepp_logan_recon(self):
         angles = np.linspace(-np.pi/2.0, np.pi/2.0, num_views, endpoint=False)
 
         try:
-            sino = svmbir.project(angles,phantom,max(num_rows,num_cols))
+            sino = svmbir.project(phantom,angles,max(num_rows,num_cols))
         except Exception as e:
             print(e)
             assert 0
@@ -80,7 +80,7 @@ def test_multires_microscopy(self):
         angles = np.linspace(-tilt_angle, tilt_angle, num_views)
 
         # Generate sinogram by projecting phantom
-        sino = svmbir.project(angles, phantom, max(num_rows, num_cols))
+        sino = svmbir.project(phantom, angles, max(num_rows, num_cols))
 
         # Determine resulting number of views, slices, and channels
         (num_views, num_slices, num_channels) = sino.shape