Repository: rmjarvis/TreeCorr (language: python)

Issue:
write_patch_results
As far as I can see, including the write_patch_results argument in the write method writes additional HDUs with the DD pair counts corresponding to each patch.  Is it possible to write out the correlation function and/or all of the pair counts specified in the write function?  A use is case is say I want to fit a function to each patch estimate to obtain more robust covariances on the fit parameters.

I am currently doing:
`
dd.process(gcat)
dr.process(gcat, rcat)
rr.process(rcat)
dd.calculateXi(rr=rr, dr=dr)
dd.write('filename.fits', rr=rr, dr=dr, write_patch_results=True)
`
As a workaround, I assume I can write out the patch counts into separate files for DD, DR, and RR pairs, and then compute w(theta) for each patch, but this seems like an unnecessary faff.


Discussion / hints:
Sure.  Both of those sound like reasonable feature additions.  I think the rr, dr results dicts shouldn't be too hard to implement.  As you say, for now the workaround is to write each one separately into their own files.  

You can also separately write out the covariance matrix from the start, rather than relying on reading in the TreeCorr output file to reconstruct it.  E.g. `np.savez(cov_file_name, dd.cov)`.  

Since the covariance matrix is 2d, I'll have to think what the best way to store that is in the TreeCorr output file.  (FITS and HDF5 are probably easy, but ASCII will take a little thought.)  But numpy's save function does a nice job there, so you can just use that for now.
Re-reading your text above (as I'm starting to think about implementation), I realize that you talked about computing w(theta) for each patch, which is probably not anything you ever want to do.  That's not the best way to calculate a covariance matrix, since you miss the cross-patch pairs.  

The unit test that recovers a covariance matrix from the three (DD, DR, RR) output files is [here](https://github.com/rmjarvis/TreeCorr/blob/releases/4.3/tests/test_patch.py#L1474), so you might want to use that as a model for how to do it in your code.  Basically:
```
# In main code that computes dd, dr, rr
dd.write(dd_file_name, dr=dr, rr=rr, write_patch_results=True)
rr.write(rr_file_name, write_patch_results=True)
dr.write(dr_file_name, write_patch_results=True)

# In other code that needs covariance matrix
dd = treecorr.NNCorrelation(...)
rr = treecorr.NNCorrelation(...)
dr = treecorr.NNCorrelation(...)
dd.read(dd_file_name)
rr.read(rr_file_name)
dr.read(dr_file_name)
dd.calculateXi(rr=rr, dr=dr)
cov = dd.estimate_cov('jackknife')
```

It's still more boilerplate than we really need of course, but maybe simpler than what you seemed to be describing that you currently do.  I'm working on making the workflow look like this:
```
# In main code that computes dd, dr, rr
dd.write(file_name, dr=dr, rr=rr, write_patch_results=True)

# In other code that needs covariance matrix
dd = treecorr.NNCorrelation(...)
dd.read(file_name)
cov = dd.estimate_cov('jackknife')
```
Or even better, I think I can probably make the read just be:
```
dd = treecorr.NNCorrelation.from_file(file_name)
```
which would be even nicer API I think.

Fix patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 68edec31..6b5ffe45 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -37,6 +37,7 @@ API Changes
 - Changed estimate_cov with method='shot' to only return the diagonal, rather than gratuitously
   making a full, mostly empty diagonal matrix. (#166)
 - Changed name of Catalog.write kwarg from cat_precision to just precision. (#169)
+- Added additionaly information in the header of output files to enable ``from_file``. (#172)
 
 
 Performance improvements
@@ -77,6 +78,14 @@ New features
   three-point method, for which it is the default. (#169)
 - Added ``angle_slop`` option to separately tune the allowed angular slop from using cells,
   irrespective of the binning. (#170)
+- Added ``algo`` option to 3-point ``process`` functions to conrol whether to use new
+  multipole algorithm or the old triangle algorithm. (#171)
+- Added serialization of rr, dr, etc. when writing with write_patch_results=True option,
+  so you no longer have to separately write files for them to recover the covariance. (#172)
+- Added :ref:`from_file <GGCorrlation.from_file>` class methods to construct a Correlation
+  object from a file without needing to know the correct configuration parameters. (#172)
+- Added ``write_cov`` option to write functions to include the covariance in the output file.
+  (#172)
 
 
 Bug fixes
diff --git a/treecorr/catalog.py b/treecorr/catalog.py
index 2c628198..66e1fe60 100644
--- a/treecorr/catalog.py
+++ b/treecorr/catalog.py
@@ -758,10 +758,12 @@ def __init__(self, file_name=None, config=None, *, num=0, logger=None, is_rand=F
                 delimiter = self.config.get('delimiter',None)
                 comment_marker = self.config.get('comment_marker','#')
                 try:
-                    self.reader = PandasReader(file_name, delimiter, comment_marker)
+                    self.reader = PandasReader(file_name, delimiter=delimiter,
+                                               comment_marker=comment_marker)
                 except ImportError:
                     self._pandas_warning()
-                    self.reader = AsciiReader(file_name, delimiter, comment_marker)
+                    self.reader = AsciiReader(file_name, delimiter=delimiter,
+                                              comment_marker=comment_marker)
                 self._check_file(file_name, self.reader, num, is_rand)
 
             self.file_type = file_type
@@ -1576,51 +1578,51 @@ def _check_file(self, file_name, reader, num=0, is_rand=False):
             if x_col != '0':
                 x_ext = get_from_list(self.config, 'x_ext', num, str, ext)
                 y_ext = get_from_list(self.config, 'y_ext', num, str, ext)
-                if x_col not in reader.names(x_ext):
+                if x_col not in reader.names(ext=x_ext):
                     raise ValueError("x_col=%s is invalid for file %s"%(x_col,file_name))
-                if y_col not in reader.names(y_ext):
+                if y_col not in reader.names(ext=y_ext):
                     raise ValueError("y_col=%s is invalid for file %s"%(y_col, file_name))
                 if z_col != '0':
                     z_ext = get_from_list(self.config, 'z_ext', num, str, ext)
-                    if z_col not in reader.names(z_ext):
+                    if z_col not in reader.names(ext=z_ext):
                         raise ValueError("z_col=%s is invalid for file %s"%(z_col, file_name))
             else:
                 ra_ext = get_from_list(self.config, 'ra_ext', num, str, ext)
                 dec_ext = get_from_list(self.config, 'dec_ext', num, str, ext)
-                if ra_col not in reader.names(ra_ext):
+                if ra_col not in reader.names(ext=ra_ext):
                     raise ValueError("ra_col=%s is invalid for file %s"%(ra_col, file_name))
-                if dec_col not in reader.names(dec_ext):
+                if dec_col not in reader.names(ext=dec_ext):
                     raise ValueError("dec_col=%s is invalid for file %s"%(dec_col, file_name))
                 if r_col != '0':
                     r_ext = get_from_list(self.config, 'r_ext', num, str, ext)
-                    if r_col not in reader.names(r_ext):
+                    if r_col not in reader.names(ext=r_ext):
                         raise ValueError("r_col=%s is invalid for file %s"%(r_col, file_name))
 
             if w_col != '0':
                 w_ext = get_from_list(self.config, 'w_ext', num, str, ext)
-                if w_col not in reader.names(w_ext):
+                if w_col not in reader.names(ext=w_ext):
                     raise ValueError("w_col=%s is invalid for file %s"%(w_col, file_name))
 
             if wpos_col != '0':
                 wpos_ext = get_from_list(self.config, 'wpos_ext', num, str, ext)
-                if wpos_col not in reader.names(wpos_ext):
+                if wpos_col not in reader.names(ext=wpos_ext):
                     raise ValueError("wpos_col=%s is invalid for file %s"%(wpos_col, file_name))
 
             if flag_col != '0':
                 flag_ext = get_from_list(self.config, 'flag_ext', num, str, ext)
-                if flag_col not in reader.names(flag_ext):
+                if flag_col not in reader.names(ext=flag_ext):
                     raise ValueError("flag_col=%s is invalid for file %s"%(flag_col, file_name))
 
             if patch_col != '0':
                 patch_ext = get_from_list(self.config, 'patch_ext', num, str, ext)
-                if patch_col not in reader.names(patch_ext):
+                if patch_col not in reader.names(ext=patch_ext):
                     raise ValueError("patch_col=%s is invalid for file %s"%(patch_col, file_name))
 
             if is_rand: return
 
             if k_col != '0':
                 k_ext = get_from_list(self.config, 'k_ext', num, str, ext)
-                if k_col not in reader.names(k_ext):
+                if k_col not in reader.names(ext=k_ext):
                     if isKColRequired(self.orig_config,num):
                         raise ValueError("k_col=%s is invalid for file %s"%(k_col, file_name))
                     else:
@@ -1632,8 +1634,8 @@ def _check_file(self, file_name, reader, num=0, is_rand=False):
             if v1_col != '0':
                 v1_ext = get_from_list(self.config, 'v1_ext', num, str, ext)
                 v2_ext = get_from_list(self.config, 'v2_ext', num, str, ext)
-                if (v1_col not in reader.names(v1_ext) or
-                    v2_col not in reader.names(v2_ext)):
+                if (v1_col not in reader.names(ext=v1_ext) or
+                    v2_col not in reader.names(ext=v2_ext)):
                     if isVColRequired(self.orig_config,num):
                         raise ValueError(
                             "v1_col, v2_col=(%s, %s) are invalid for file %s"%(
@@ -1647,8 +1649,8 @@ def _check_file(self, file_name, reader, num=0, is_rand=False):
             if g1_col != '0':
                 g1_ext = get_from_list(self.config, 'g1_ext', num, str, ext)
                 g2_ext = get_from_list(self.config, 'g2_ext', num, str, ext)
-                if (g1_col not in reader.names(g1_ext) or
-                    g2_col not in reader.names(g2_ext)):
+                if (g1_col not in reader.names(ext=g1_ext) or
+                    g2_col not in reader.names(ext=g2_ext)):
                     if isGColRequired(self.orig_config,num):
                         raise ValueError(
                             "g1_col, g2_col=(%s, %s) are invalid for file %s"%(
@@ -1662,8 +1664,8 @@ def _check_file(self, file_name, reader, num=0, is_rand=False):
             if t1_col != '0':
                 t1_ext = get_from_list(self.config, 't1_ext', num, str, ext)
                 t2_ext = get_from_list(self.config, 't2_ext', num, str, ext)
-                if (t1_col not in reader.names(t1_ext) or
-                    t2_col not in reader.names(t2_ext)):
+                if (t1_col not in reader.names(ext=t1_ext) or
+                    t2_col not in reader.names(ext=t2_ext)):
                     if isTColRequired(self.orig_config,num):
                         raise ValueError(
                             "t1_col, t2_col=(%s, %s) are invalid for file %s"%(
@@ -1677,8 +1679,8 @@ def _check_file(self, file_name, reader, num=0, is_rand=False):
             if q1_col != '0':
                 q1_ext = get_from_list(self.config, 'q1_ext', num, str, ext)
                 q2_ext = get_from_list(self.config, 'q2_ext', num, str, ext)
-                if (q1_col not in reader.names(q1_ext) or
-                    q2_col not in reader.names(q2_ext)):
+                if (q1_col not in reader.names(ext=q1_ext) or
+                    q2_col not in reader.names(ext=q2_ext)):
                     if isQColRequired(self.orig_config,num):
                         raise ValueError(
                             "q1_col, q2_col=(%s, %s) are invalid for file %s"%(
@@ -1820,7 +1822,7 @@ def set_patch(data, patch_col):
             # Also, if we are only reading in one patch, we should adjust s before doing this.
             if self._single_patch is not None:
                 if patch_col != '0':
-                    data[patch_col] = reader.read(patch_col, s, patch_ext)
+                    data[patch_col] = reader.read(patch_col, s, ext=patch_ext)
                     all_cols.remove(patch_col)
                     set_patch(data, patch_col)
                 elif self._centers is not None:
@@ -1830,7 +1832,7 @@ def set_patch(data, patch_col):
                         all_cols.remove(c)
                     for ext in all_exts:
                         use_cols1 = [c for c in pos_cols if col_by_ext[c] == ext]
-                        data1 = reader.read(use_cols1, s, ext)
+                        data1 = reader.read(use_cols1, s, ext=ext)
                         for c in use_cols1:
                             data[c] = data1[c]
                     set_pos(data, x_col, y_col, z_col, ra_col, dec_col, r_col)
@@ -1855,10 +1857,10 @@ def set_patch(data, patch_col):
             # Now read the rest using the updated s
             for ext in all_exts:
                 use_cols1 = [c for c in all_cols
-                             if col_by_ext[c] == ext and c in reader.names(ext)]
+                             if col_by_ext[c] == ext and c in reader.names(ext=ext)]
                 if len(use_cols1) == 0:
                     continue
-                data1 = reader.read(use_cols1, s, ext)
+                data1 = reader.read(use_cols1, s, ext=ext)
                 for c in use_cols1:
                     data[c] = data1[c]
 
@@ -1886,33 +1888,33 @@ def set_patch(data, patch_col):
             # Skip k,v,g,t,q if this file is a random catalog
             if not is_rand:
                 # Set k
-                if k_col in reader.names(k_ext):
+                if k_col in reader.names(ext=k_ext):
                     self._k = data[k_col].astype(float)
                     self.logger.debug('read k')
 
                 # Set v1,v2
-                if v1_col in reader.names(v1_ext):
+                if v1_col in reader.names(ext=v1_ext):
                     self._v1 = data[v1_col].astype(float)
                     self.logger.debug('read v1')
                     self._v2 = data[v2_col].astype(float)
                     self.logger.debug('read v2')
 
                 # Set g1,g2
-                if g1_col in reader.names(g1_ext):
+                if g1_col in reader.names(ext=g1_ext):
                     self._g1 = data[g1_col].astype(float)
                     self.logger.debug('read g1')
                     self._g2 = data[g2_col].astype(float)
                     self.logger.debug('read g2')
 
                 # Set t1,t2
-                if t1_col in reader.names(t1_ext):
+                if t1_col in reader.names(ext=t1_ext):
                     self._t1 = data[t1_col].astype(float)
                     self.logger.debug('read t1')
                     self._t2 = data[t2_col].astype(float)
                     self.logger.debug('read t2')
 
                 # Set q1,q2
-                if q1_col in reader.names(q1_ext):
+                if q1_col in reader.names(ext=q1_ext):
                     self._q1 = data[q1_col].astype(float)
                     self.logger.debug('read q1')
                     self._q2 = data[q2_col].astype(float)
diff --git a/treecorr/config.py b/treecorr/config.py
index 8b7eae2a..71b95332 100644
--- a/treecorr/config.py
+++ b/treecorr/config.py
@@ -418,3 +418,15 @@ def merge_config(config, kwargs, valid_params, aliases=None):
             if key in valid_params and key not in kwargs:
                 kwargs[key] = value
     return check_config(kwargs, valid_params, aliases)
+
+def make_minimal_config(config, valid_params):
+    """Make a minimal version of a config dict, excluding any values that are the default.
+
+    Parameters:
+        config (dict):          The source config (will not be modified)
+        valid_params (dict):    A dict of valid parameters that are allowed for this usage.
+
+    Returns:
+        minimal_config  The dict without any default values.
+    """
+    return { k:v for k,v in config.items() if k in valid_params and v != valid_params[k][2] }
diff --git a/treecorr/corr2base.py b/treecorr/corr2base.py
index deab9c83..24a38476 100644
--- a/treecorr/corr2base.py
+++ b/treecorr/corr2base.py
@@ -23,7 +23,7 @@
 import collections
 
 from . import _treecorr
-from .config import merge_config, setup_logger, get
+from .config import merge_config, setup_logger, get, make_minimal_config
 from .util import parse_metric, metric_enum, coord_enum, set_omp_threads, lazy_property
 from .catalog import Catalog
 
@@ -512,6 +512,12 @@ def rng(self):
             self._rng = np.random.default_rng()
         return self._rng
 
+    # Helper for making a nice repr that doesn't list all params that are just defaults.
+    @property
+    def _repr_kwargs(self):
+        kwargs = make_minimal_config(self.config, Corr2._valid_params)
+        return ', '.join(f'{k}={v!r}' for k,v in kwargs.items())
+
     # Properties for all the read-only attributes ("ro" stands for "read-only")
     @property
     def output_dots(self): return self._ro.output_dots
@@ -1393,7 +1399,10 @@ def make_gen(self):
     def _bootstrap_pairs(self, index):
         return self.BootstrapPairIterator(self.results, self.npatch1, self.npatch2, index, self._ok)
 
-    def _write(self, writer, name, write_patch_results, zero_tot=False):
+    def _write(self, writer, name, write_patch_results, write_cov=False, zero_tot=False):
+        if name is None and (write_patch_results or write_cov):
+            # HDF doesn't work right with multiple groups unless they all have non-empty names.
+            name = 'main'
         # These helper properties define what to write for each class.
         col_names = self._write_col_names
         data = self._write_data
@@ -1416,6 +1425,8 @@ def _write(self, writer, name, write_patch_results, zero_tot=False):
                         i += 1
                 params['num_zero_patch'] = i
             params['num_patch_pairs'] = num_patch_pairs
+        if write_cov:
+            params['cov_shape'] = self.cov.shape
 
         writer.write(col_names, data, params=params, ext=name)
         if write_patch_results:
@@ -1431,13 +1442,17 @@ def _write(self, writer, name, write_patch_results, zero_tot=False):
                 writer.write(col_names, data, params=params, ext=pp_name)
                 i += 1
             assert i == num_patch_pairs
+        if write_cov:
+            writer.write_array(self.cov, ext='cov')
 
-    def _read(self, reader, name=None):
+    def _read(self, reader, name=None, params=None):
         name = 'main' if 'main' in reader and name is None else name
-        params = reader.read_params(ext=name)
+        if params is None:
+            params = reader.read_params(ext=name)
         num_rows = params.get('num_rows', None)
         num_patch_pairs = params.get('num_patch_pairs', 0)
         num_zero_patch = params.get('num_zero_patch', 0)
+        cov_shape = params.get('cov_shape', None)
         name = 'main' if num_patch_pairs and name is None else name
         data = reader.read_data(max_rows=num_rows, ext=name)
 
@@ -1458,6 +1473,10 @@ def _read(self, reader, name=None):
             corr._read_from_data(data, params)
             key = eval(params['key'])
             self.results[key] = corr
+        if cov_shape is not None:
+            if isinstance(cov_shape, str):
+                cov_shape = eval(cov_shape)
+            self._cov = reader.read_array(cov_shape, ext='cov')
 
 def estimate_multi_cov(corrs, method, *, func=None, comm=None):
     """Estimate the covariance matrix of multiple statistics.
diff --git a/treecorr/corr3base.py b/treecorr/corr3base.py
index 54bd74e4..225f404d 100644
--- a/treecorr/corr3base.py
+++ b/treecorr/corr3base.py
@@ -21,7 +21,7 @@
 import coord
 
 from . import _treecorr
-from .config import merge_config, setup_logger, get
+from .config import merge_config, setup_logger, get, make_minimal_config
 from .util import parse_metric, metric_enum, coord_enum, set_omp_threads, lazy_property
 from .util import make_reader
 from .corr2base import estimate_multi_cov, build_multi_cov_design_matrix, Corr2
@@ -732,6 +732,12 @@ def rng(self):
             self._rng = np.random.default_rng()
         return self._rng
 
+    # Helper for making a nice repr that doesn't list all params that are just defaults.
+    @property
+    def _repr_kwargs(self):
+        kwargs = make_minimal_config(self.config, Corr3._valid_params)
+        return ', '.join(f'{k}={v!r}' for k,v in kwargs.items())
+
     # Properties for all the read-only attributes ("ro" stands for "read-only")
     @property
     def output_dots(self): return self._ro.output_dots
@@ -1863,7 +1869,9 @@ def _bootstrap_pairs(self, indx):
                     [ (i,j,k) for i in indx for j in indx if i!=j
                               for k in indx if self._ok[i,j,k] and (i!=k and j!=k) ])
 
-    def _write(self, writer, name, write_patch_results, zero_tot=False):
+    def _write(self, writer, name, write_patch_results, write_cov=False, zero_tot=False):
+        if name is None and (write_patch_results or write_cov):
+            name = 'main'
         # These helper properties define what to write for each class.
         col_names = self._write_col_names
         data = self._write_data
@@ -1887,6 +1895,8 @@ def _write(self, writer, name, write_patch_results, zero_tot=False):
                         i += 1
                 params['num_zero_patch'] = i
             params['num_patch_tri'] = num_patch_tri
+        if write_cov:
+            params['cov_shape'] = self.cov.shape
 
         writer.write(col_names, data, params=params, ext=name)
         if write_patch_results:
@@ -1902,13 +1912,17 @@ def _write(self, writer, name, write_patch_results, zero_tot=False):
                 writer.write(col_names, data, params=params, ext=pp_name)
                 i += 1
             assert i == num_patch_tri
+        if write_cov:
+            writer.write_array(self.cov, ext='cov')
 
-    def _read(self, reader, name=None):
+    def _read(self, reader, name=None, params=None):
         name = 'main' if 'main' in reader and name is None else name
-        params = reader.read_params(ext=name)
+        if params is None:
+            params = reader.read_params(ext=name)
         num_rows = params.get('num_rows', None)
         num_patch_tri = params.get('num_patch_tri', 0)
         num_zero_patch = params.get('num_zero_patch', 0)
+        cov_shape = params.get('cov_shape', None)
         name = 'main' if num_patch_tri and name is None else name
         data = reader.read_data(max_rows=num_rows, ext=name)
 
@@ -1929,3 +1943,7 @@ def _read(self, reader, name=None):
             corr._read_from_data(data, params)
             key = eval(params['key'])
             self.results[key] = corr
+        if cov_shape is not None:
+            if isinstance(cov_shape, str):
+                cov_shape = eval(cov_shape)
+            self._cov = reader.read_array(cov_shape, ext='cov')
diff --git a/treecorr/ggcorrelation.py b/treecorr/ggcorrelation.py
index 8d13a323..41fe33ee 100644
--- a/treecorr/ggcorrelation.py
+++ b/treecorr/ggcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarG
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class GGCorrelation(Corr2):
@@ -174,7 +175,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'GGCorrelation(config=%r)'%self.config
+        return f'GGCorrelation({self._repr_kwargs})'
 
     def process_auto(self, cat, *, metric=None, num_threads=None):
         """Process a single catalog, accumulating the auto-correlation.
@@ -442,7 +443,8 @@ def process(self, cat1, cat2=None, *, metric=None, num_threads=None, comm=None,
             self._processed_cats2.clear()
 
 
-    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False):
+    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False,
+              write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         The output file will include the following columns:
@@ -477,12 +479,12 @@ def write(self, file_name, *, file_type=None, precision=None, write_patch_result
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing GG correlations to %s',file_name)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -500,14 +502,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create a GGCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: A GGCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building GGCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading GG correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
@@ -517,8 +550,8 @@ def read(self, file_name, *, file_type=None):
 
         Parameters:
             file_name (str):    The name of the file to read in.
-            file_type (str):    The type of file ('ASCII' or 'FITS').  (default: determine the type
-                                automatically from the extension of file_name.)
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
         """
         self.logger.info('Reading GG correlations from %s',file_name)
         with make_reader(file_name, file_type, self.logger) as reader:
diff --git a/treecorr/gggcorrelation.py b/treecorr/gggcorrelation.py
index f3d26fde..9a28f88e 100644
--- a/treecorr/gggcorrelation.py
+++ b/treecorr/gggcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarG
 from .corr3base import Corr3
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class GGGCorrelation(Corr3):
@@ -289,7 +290,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'GGGCorrelation(config=%r)'%self.config
+        return f'GGGCorrelation({self._repr_kwargs})'
 
     def process_auto(self, cat, *, metric=None, num_threads=None):
         """Process a single catalog, accumulating the auto-correlation.
@@ -965,7 +966,8 @@ def toSAS(self, *, target=None, **kwargs):
 
         return sas
 
-    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False):
+    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False,
+              write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         As described in the doc string for `GGGCorrelation`, we use the "natural components" of
@@ -1057,12 +1059,12 @@ def write(self, file_name, *, file_type=None, precision=None, write_patch_result
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing GGG correlations to %s',file_name)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -1114,14 +1116,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr3._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create a GGGCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: A GGGCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building GGGCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr3._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading GGG correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/kgcorrelation.py b/treecorr/kgcorrelation.py
index 3f4f8ee7..e8658e86 100644
--- a/treecorr/kgcorrelation.py
+++ b/treecorr/kgcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarG, calculateVarK
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class KGCorrelation(Corr2):
@@ -169,7 +170,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'KGCorrelation(config=%r)'%self.config
+        return f'KGCorrelation({self._repr_kwargs})'
 
     def process_cross(self, cat1, cat2, *, metric=None, num_threads=None):
         """Process a single pair of catalogs, accumulating the cross-correlation.
@@ -355,7 +356,8 @@ def process(self, cat1, cat2, *, metric=None, num_threads=None, comm=None, low_m
             self._processed_cats1.clear()
             self._processed_cats2.clear()
 
-    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False):
+    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False,
+              write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         The output file will include the following columns:
@@ -388,12 +390,12 @@ def write(self, file_name, *, file_type=None, precision=None, write_patch_result
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing KG correlations to %s',file_name)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -409,14 +411,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create a KGCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: A KGCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building KGCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading KG correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/kkcorrelation.py b/treecorr/kkcorrelation.py
index c6bdd4b4..bc7cad55 100644
--- a/treecorr/kkcorrelation.py
+++ b/treecorr/kkcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarK
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class KKCorrelation(Corr2):
@@ -170,7 +171,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'KKCorrelation(config=%r)'%self.config
+        return f'KKCorrelation({self._repr_kwargs})'
 
     def process_auto(self, cat, *, metric=None, num_threads=None):
         """Process a single catalog, accumulating the auto-correlation.
@@ -399,7 +400,8 @@ def process(self, cat1, cat2=None, *, metric=None, num_threads=None, comm=None,
             self._processed_cats1.clear()
             self._processed_cats2.clear()
 
-    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False):
+    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False,
+              write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         The output file will include the following columns:
@@ -430,12 +432,12 @@ def write(self, file_name, *, file_type=None, precision=None, write_patch_result
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing KK correlations to %s',file_name)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -450,14 +452,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create a KKCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: A KKCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building KKCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading KK correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/kkkcorrelation.py b/treecorr/kkkcorrelation.py
index eb1eb224..d09be0e4 100644
--- a/treecorr/kkkcorrelation.py
+++ b/treecorr/kkkcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarK
 from .corr3base import Corr3
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class KKKCorrelation(Corr3):
@@ -238,7 +239,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'KKKCorrelation(config=%r)'%self.config
+        return f'KKKCorrelation({self._repr_kwargs})'
 
     def process_auto(self, cat, *, metric=None, num_threads=None):
         """Process a single catalog, accumulating the auto-correlation.
@@ -763,7 +764,8 @@ def toSAS(self, *, target=None, **kwargs):
 
         return sas
 
-    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False):
+    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False,
+              write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         For bin_type = LogRUV, the output file will include the following columns:
@@ -842,12 +844,12 @@ def write(self, file_name, *, file_type=None, precision=None, write_patch_result
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing KKK correlations to %s',file_name)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -893,14 +895,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr3._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create a KKKCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: A KKKCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building KKKCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr3._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading KKK correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/kqcorrelation.py b/treecorr/kqcorrelation.py
index afd3799c..f7999d9f 100644
--- a/treecorr/kqcorrelation.py
+++ b/treecorr/kqcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarQ, calculateVarK
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class KQCorrelation(Corr2):
@@ -162,7 +163,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'KQCorrelation(config=%r)'%self.config
+        return f'KQCorrelation({self._repr_kwargs})'
 
     def process_cross(self, cat1, cat2, *, metric=None, num_threads=None):
         """Process a single pair of catalogs, accumulating the cross-correlation.
@@ -348,7 +349,8 @@ def process(self, cat1, cat2, *, metric=None, num_threads=None, comm=None, low_m
             self._processed_cats1.clear()
             self._processed_cats2.clear()
 
-    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False):
+    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False,
+              write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         The output file will include the following columns:
@@ -381,12 +383,12 @@ def write(self, file_name, *, file_type=None, precision=None, write_patch_result
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing KQ correlations to %s',file_name)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -402,14 +404,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create a KQCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: A KQCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building KQCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading KQ correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/ktcorrelation.py b/treecorr/ktcorrelation.py
index 49955d30..fd7a2d8e 100644
--- a/treecorr/ktcorrelation.py
+++ b/treecorr/ktcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarT, calculateVarK
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class KTCorrelation(Corr2):
@@ -162,7 +163,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'KTCorrelation(config=%r)'%self.config
+        return f'KTCorrelation({self._repr_kwargs})'
 
     def process_cross(self, cat1, cat2, *, metric=None, num_threads=None):
         """Process a single pair of catalogs, accumulating the cross-correlation.
@@ -348,7 +349,8 @@ def process(self, cat1, cat2, *, metric=None, num_threads=None, comm=None, low_m
             self._processed_cats1.clear()
             self._processed_cats2.clear()
 
-    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False):
+    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False,
+              write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         The output file will include the following columns:
@@ -381,12 +383,12 @@ def write(self, file_name, *, file_type=None, precision=None, write_patch_result
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing KT correlations to %s',file_name)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -402,14 +404,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create a KTCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: A KTCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building KTCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading KT correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/kvcorrelation.py b/treecorr/kvcorrelation.py
index 0abdbc88..22fd4ac2 100644
--- a/treecorr/kvcorrelation.py
+++ b/treecorr/kvcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarV, calculateVarK
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class KVCorrelation(Corr2):
@@ -162,7 +163,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'KVCorrelation(config=%r)'%self.config
+        return f'KVCorrelation({self._repr_kwargs})'
 
     def process_cross(self, cat1, cat2, *, metric=None, num_threads=None):
         """Process a single pair of catalogs, accumulating the cross-correlation.
@@ -348,7 +349,8 @@ def process(self, cat1, cat2, *, metric=None, num_threads=None, comm=None, low_m
             self._processed_cats1.clear()
             self._processed_cats2.clear()
 
-    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False):
+    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False,
+              write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         The output file will include the following columns:
@@ -381,12 +383,12 @@ def write(self, file_name, *, file_type=None, precision=None, write_patch_result
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing KV correlations to %s',file_name)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -402,14 +404,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create a KVCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: A KVCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building KVCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading KV correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/ngcorrelation.py b/treecorr/ngcorrelation.py
index 334ef34e..9b4c4145 100644
--- a/treecorr/ngcorrelation.py
+++ b/treecorr/ngcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarG
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class NGCorrelation(Corr2):
@@ -178,7 +179,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'NGCorrelation(config=%r)'%self.config
+        return f'NGCorrelation({self._repr_kwargs})'
 
     def process_cross(self, cat1, cat2, *, metric=None, num_threads=None):
         """Process a single pair of catalogs, accumulating the cross-correlation.
@@ -447,7 +448,7 @@ def _calculate_xi_from_pairs(self, pairs):
             self.xi -= self._rg.xi
 
     def write(self, file_name, *, rg=None, file_type=None, precision=None,
-              write_patch_results=False):
+              write_patch_results=False, write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         - If rg is None, the simple correlation function :math:`\langle \gamma_T\rangle` is used.
@@ -488,13 +489,13 @@ def write(self, file_name, *, rg=None, file_type=None, precision=None,
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing NG correlations to %s',file_name)
         self.calculateXi(rg=rg)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -509,14 +510,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create an NGCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: An NGCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building NGCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading NG correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/nkcorrelation.py b/treecorr/nkcorrelation.py
index 8958846d..174521dc 100644
--- a/treecorr/nkcorrelation.py
+++ b/treecorr/nkcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarK
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class NKCorrelation(Corr2):
@@ -180,7 +181,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'NKCorrelation(config=%r)'%self.config
+        return f'NKCorrelation({self._repr_kwargs})'
 
     def process_cross(self, cat1, cat2, *, metric=None, num_threads=None):
         """Process a single pair of catalogs, accumulating the cross-correlation.
@@ -437,7 +438,7 @@ def _calculate_xi_from_pairs(self, pairs):
             self.xi -= self._rk.xi
 
     def write(self, file_name, * ,rk=None, file_type=None, precision=None,
-              write_patch_results=False):
+              write_patch_results=False, write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         - If rk is None, the simple correlation function :math:`\langle \kappa \rangle(R)` is
@@ -477,13 +478,13 @@ def write(self, file_name, * ,rk=None, file_type=None, precision=None,
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing NK correlations to %s',file_name)
         self.calculateXi(rk=rk)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -498,14 +499,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create an NKCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: An NKCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building NKCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading NK correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/nncorrelation.py b/treecorr/nncorrelation.py
index f26f77e7..7ef07dad 100644
--- a/treecorr/nncorrelation.py
+++ b/treecorr/nncorrelation.py
@@ -20,6 +20,7 @@
 from . import _treecorr
 from .corr2base import Corr2
 from .util import make_writer, make_reader, lazy_property
+from .config import make_minimal_config
 
 
 class NNCorrelation(Corr2):
@@ -108,6 +109,7 @@ def __init__(self, config=None, *, logger=None, **kwargs):
         self._write_rr = None
         self._write_dr = None
         self._write_rd = None
+        self._write_patch_results = False
         self._cov = None
         self._var_num = 0
         self.logger.debug('Finished building NNCorr')
@@ -163,12 +165,13 @@ def copy(self):
                 # never add more to it after the copy, so shallow copy is fine.
                 ret.__dict__[key] = item
         ret._corr = None # We'll want to make a new one of these if we need it.
-        if self._rd is not None:
-            ret._rd = self._rd.copy()
-        if self._dr is not None:
-            ret._dr = self._dr.copy()
-        if self._rr is not None:
+        # True is possible during read before we finish reading in these attributes.
+        if self._rr is not None and self._rr is not True:
             ret._rr = self._rr.copy()
+        if self._dr is not None and self._dr is not True:
+            ret._dr = self._dr.copy()
+        if self._rd is not None and self._rd is not True:
+            ret._rd = self._rd.copy()
         if self._cov is not None:
             ret._cov = self._cov.copy()
         return ret
@@ -195,6 +198,7 @@ def _zero_copy(self, tot):
         ret._corr = None
         ret._rr = ret._dr = ret._rd = None
         ret._write_rr = ret._write_dr = ret._write_rd = None
+        ret._write_patch_results = False
         ret._cov = None
         ret._logger_name = None
         # This override is really the main advantage of using this:
@@ -202,7 +206,7 @@ def _zero_copy(self, tot):
         return ret
 
     def __repr__(self):
-        return 'NNCorrelation(config=%r)'%self.config
+        return f'NNCorrelation({self._repr_kwargs})'
 
     def process_auto(self, cat, *, metric=None, num_threads=None):
         """Process a single catalog, accumulating the auto-correlation.
@@ -511,7 +515,7 @@ def calculateXi(self, *, rr, dr=None, rd=None):
         elif dr is not None and rd is None:
             self.xi = self.weight - 2.*dr.weight * drf + denom
         else:
-            self.xi = self.weight - rd.weight * rdf - dr.weight * drf + denom
+            self.xi = self.weight - dr.weight * drf - rd.weight * rdf + denom
 
         # Divide by RR in all cases.
         if np.any(rr.weight == 0):
@@ -570,7 +574,7 @@ def calculateXi(self, *, rr, dr=None, rd=None):
                                    rd.npatch1 not in (self.npatch1, 1)):
                 raise RuntimeError("RD must be run with the same patches as DD")
 
-            # If there are any rr,rd,dr patch pairs that aren't in results (because dr is a cross
+            # If there are any rr,dr,rd patch pairs that aren't in results (because dr is a cross
             # correlation, and dd,rr may be auto-correlations, or because the d catalogs has some
             # patches with no items), then we need to add some dummy results to make sure all the
             # right pairs are computed when we make the vectors for the covariance matrix.
@@ -657,13 +661,13 @@ def _calculate_xi_from_pairs(self, pairs):
         elif self._dr is not None and self._rd is None:
             xi = dd - 2.*dr * drf + denom
         else:
-            xi = dd - rd * rdf - dr * drf + denom
+            xi = dd - dr * drf - rd * rdf + denom
         denom[denom == 0] = 1  # Guard against division by zero.
         self.xi = xi / denom
         self._rr_weight = denom
 
     def write(self, file_name, *, rr=None, dr=None, rd=None, file_type=None, precision=None,
-              write_patch_results=False):
+              write_patch_results=False, write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         rr is the `NNCorrelation` function for random points.
@@ -714,19 +718,32 @@ def write(self, file_name, *, rr=None, dr=None, rd=None, file_type=None, precisi
                                     dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):       Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing NN correlations to %s',file_name)
         # Temporary attributes, so the helper functions can access them.
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         self._write_rr = rr
         self._write_dr = dr
         self._write_rd = rd
+        self._write_patch_results = write_patch_results
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results, zero_tot=True)
+            self._write(writer, None, write_patch_results, write_cov=write_cov, zero_tot=True)
+            if write_patch_results:
+                # Also write out rr, dr, rd, so covariances can be computed on round trip.
+                rr = rr or self._rr
+                dr = dr or self._dr
+                rd = rd or self._rd
+                if rr:
+                    rr._write(writer, '_rr', write_patch_results, zero_tot=True)
+                if dr:
+                    dr._write(writer, '_dr', write_patch_results, zero_tot=True)
+                if rd:
+                    rd._write(writer, '_rd', write_patch_results, zero_tot=True)
         self._write_rr = None
         self._write_dr = None
         self._write_rd = None
+        self._write_patch_results = False
 
     @property
     def _write_col_names(self):
@@ -776,14 +793,50 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'tot' : self.tot, 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['tot'] = self.tot
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        if self._write_patch_results:
+            params['_rr'] = bool(self._rr)
+            params['_dr'] = bool(self._dr)
+            params['_rd'] = bool(self._rd)
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create an NNCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: An NNCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building NNCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading NN correlations from %s',file_name)
+            corr._do_read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
@@ -798,7 +851,22 @@ def read(self, file_name, *, file_type=None):
         """
         self.logger.info('Reading NN correlations from %s',file_name)
         with make_reader(file_name, file_type, self.logger) as reader:
-            self._read(reader)
+            self._do_read(reader)
+
+    def _do_read(self, reader, name=None, params=None):
+        self._read(reader, name, params)
+        if self._rr:
+            rr = self.copy()
+            rr._read(reader, name='_rr')
+            self._rr = rr
+        if self._dr:
+            dr = self.copy()
+            dr._read(reader, name='_dr')
+            self._dr = dr
+        if self._rd:
+            rd = self.copy()
+            rd._read(reader, name='_rd')
+            self._rd = rd
 
     def _read_from_data(self, data, params):
         s = self.logr.shape
@@ -814,6 +882,10 @@ def _read_from_data(self, data, params):
             self.varxi = data['sigma_xi'].reshape(s)**2
         self.npatch1 = params.get('npatch1', 1)
         self.npatch2 = params.get('npatch2', 1)
+        # Note: "or None" turns False -> None
+        self._rr = params.get('_rr', None) or None
+        self._dr = params.get('_dr', None) or None
+        self._rd = params.get('_rd', None) or None
 
     def calculateNapSq(self, *, rr, R=None, dr=None, rd=None, m2_uform=None):
         r"""Calculate the corrollary to the aperture mass statistics for counts.
diff --git a/treecorr/nnncorrelation.py b/treecorr/nnncorrelation.py
index 2411643f..12086fa6 100644
--- a/treecorr/nnncorrelation.py
+++ b/treecorr/nnncorrelation.py
@@ -20,6 +20,7 @@
 from . import _treecorr
 from .corr3base import Corr3
 from .util import make_writer, make_reader, lazy_property
+from .config import make_minimal_config
 
 
 class NNNCorrelation(Corr3):
@@ -176,6 +177,7 @@ def __init__(self, config=None, *, logger=None, **kwargs):
         self._write_rrr = None
         self._write_drr = None
         self._write_rdd = None
+        self._write_patch_results = False
         self.zeta = None
         self._cov = None
         self._var_num = 0
@@ -230,12 +232,15 @@ def copy(self):
                 # never add more to it after the copy, so shallow copy is fine.
                 ret.__dict__[key] = item
         ret._corr = None # We'll want to make a new one of these if we need it.
-        if self._drr is not None:
+        # True is possible during read before we finish reading in these attributes.
+        if self._rrr is not None and self._rrr is not True:
+            ret._rrr = self._rrr.copy()
+        if self._drr is not None and self._drr is not True:
             ret._drr = self._drr.copy()
-        if self._rdd is not None:
+        if self._rdd is not None and self._rdd is not True:
             ret._rdd = self._rdd.copy()
-        if self._rrr is not None:
-            ret._rrr = self._rrr.copy()
+        if self._cov is not None:
+            ret._cov = self._cov.copy()
         return ret
 
     def _zero_copy(self, tot):
@@ -263,6 +268,7 @@ def _zero_copy(self, tot):
         ret._corr = None
         ret._rrr = ret._drr = ret._rdd = None
         ret._write_rrr = ret._write_drr = ret._write_rdd = None
+        ret._write_patch_results = False
         ret._cov = None
         ret._logger_name = None
         # This override is really the main advantage of using this:
@@ -270,7 +276,7 @@ def _zero_copy(self, tot):
         return ret
 
     def __repr__(self):
-        return 'NNNCorrelation(config=%r)'%self.config
+        return f'NNNCorrelation({self._repr_kwargs})'
 
     def process_auto(self, cat, *, metric=None, num_threads=None):
         """Process a single catalog, accumulating the auto-correlation.
@@ -1025,7 +1031,7 @@ def _calculate_xi_from_pairs(self, pairs):
         self._rrr_weight = denom
 
     def write(self, file_name, *, rrr=None, drr=None, rdd=None, file_type=None, precision=None,
-              write_patch_results=False):
+              write_patch_results=False, write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         Normally, at least rrr should be provided, but if this is None, then only the
@@ -1137,18 +1143,31 @@ def write(self, file_name, *, rrr=None, drr=None, rdd=None, file_type=None, prec
                                     dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):       Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing NNN correlations to %s',file_name)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         self._write_rrr = rrr
         self._write_drr = drr
         self._write_rdd = rdd
+        self._write_patch_results = write_patch_results
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results, zero_tot=True)
+            self._write(writer, None, write_patch_results, write_cov=write_cov, zero_tot=True)
+            if write_patch_results:
+                # Also write out rr, dr, rd, so covariances can be computed on round trip.
+                rrr = rrr or self._rrr
+                drr = drr or self._drr
+                rdd = rdd or self._rdd
+                if rrr:
+                    rrr._write(writer, '_rrr', write_patch_results, zero_tot=True)
+                if drr:
+                    drr._write(writer, '_drr', write_patch_results, zero_tot=True)
+                if rdd:
+                    rdd._write(writer, '_rdd', write_patch_results, zero_tot=True)
         self._write_rrr = None
         self._write_drr = None
         self._write_rdd = None
+        self._write_patch_results = False
 
     @property
     def _write_col_names(self):
@@ -1226,14 +1245,50 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'tot' : self.tot, 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr3._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['tot'] = self.tot
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        if self._write_patch_results:
+            params['_rrr'] = bool(self._rrr)
+            params['_drr'] = bool(self._drr)
+            params['_rdd'] = bool(self._rdd)
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create an NNNCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: An NNNCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building NNNCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr3._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading NNN correlations from %s',file_name)
+            corr._do_read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
@@ -1248,7 +1303,22 @@ def read(self, file_name, *, file_type=None):
         """
         self.logger.info('Reading NNN correlations from %s',file_name)
         with make_reader(file_name, file_type, self.logger) as reader:
-            self._read(reader)
+            self._do_read(reader)
+
+    def _do_read(self, reader, name=None, params=None):
+        self._read(reader, name, params)
+        if self._rrr:
+            rrr = self.copy()
+            rrr._read(reader, name='_rrr')
+            self._rrr = rrr
+        if self._drr:
+            drr = self.copy()
+            drr._read(reader, name='_drr')
+            self._drr = drr
+        if self._rdd:
+            rdd = self.copy()
+            rdd._read(reader, name='_rdd')
+            self._rdd = rdd
 
     def _read_from_data(self, data, params):
         s = self.data_shape
@@ -1278,3 +1348,7 @@ def _read_from_data(self, data, params):
         self.npatch1 = params.get('npatch1', 1)
         self.npatch2 = params.get('npatch2', 1)
         self.npatch3 = params.get('npatch3', 1)
+        # Note: "or None" turns False -> None
+        self._rrr = params.get('_rrr', None) or None
+        self._drr = params.get('_drr', None) or None
+        self._rdd = params.get('_rdd', None) or None
diff --git a/treecorr/nqcorrelation.py b/treecorr/nqcorrelation.py
index 299e7731..ffb43c5e 100644
--- a/treecorr/nqcorrelation.py
+++ b/treecorr/nqcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarQ
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class NQCorrelation(Corr2):
@@ -177,7 +178,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'NQCorrelation(config=%r)'%self.config
+        return f'NQCorrelation({self._repr_kwargs})'
 
     def process_cross(self, cat1, cat2, *, metric=None, num_threads=None):
         """Process a single pair of catalogs, accumulating the cross-correlation.
@@ -447,7 +448,7 @@ def _calculate_xi_from_pairs(self, pairs):
             self.xi -= self._rq.xi
 
     def write(self, file_name, *, rq=None, file_type=None, precision=None,
-              write_patch_results=False):
+              write_patch_results=False, write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         - If rq is None, the simple correlation function :math:`\langle q_R\rangle` is used.
@@ -488,13 +489,13 @@ def write(self, file_name, *, rq=None, file_type=None, precision=None,
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing NQ correlations to %s',file_name)
         self.calculateXi(rq=rq)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -509,14 +510,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create an NQCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: An NQCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building NQCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading NQ correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/ntcorrelation.py b/treecorr/ntcorrelation.py
index fdce1e6c..0e32a791 100644
--- a/treecorr/ntcorrelation.py
+++ b/treecorr/ntcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarT
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class NTCorrelation(Corr2):
@@ -177,7 +178,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'NTCorrelation(config=%r)'%self.config
+        return f'NTCorrelation({self._repr_kwargs})'
 
     def process_cross(self, cat1, cat2, *, metric=None, num_threads=None):
         """Process a single pair of catalogs, accumulating the cross-correlation.
@@ -447,7 +448,7 @@ def _calculate_xi_from_pairs(self, pairs):
             self.xi -= self._rt.xi
 
     def write(self, file_name, *, rt=None, file_type=None, precision=None,
-              write_patch_results=False):
+              write_patch_results=False, write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         - If rt is None, the simple correlation function :math:`\langle t_R\rangle` is used.
@@ -488,13 +489,13 @@ def write(self, file_name, *, rt=None, file_type=None, precision=None,
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing NT correlations to %s',file_name)
         self.calculateXi(rt=rt)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -509,14 +510,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create an NTCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: An NTCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building NTCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading NT correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/nvcorrelation.py b/treecorr/nvcorrelation.py
index a1e5e3a8..a62b20e2 100644
--- a/treecorr/nvcorrelation.py
+++ b/treecorr/nvcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarV
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class NVCorrelation(Corr2):
@@ -177,7 +178,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'NVCorrelation(config=%r)'%self.config
+        return f'NVCorrelation({self._repr_kwargs})'
 
     def process_cross(self, cat1, cat2, *, metric=None, num_threads=None):
         """Process a single pair of catalogs, accumulating the cross-correlation.
@@ -446,7 +447,7 @@ def _calculate_xi_from_pairs(self, pairs):
             self.xi -= self._rv.xi
 
     def write(self, file_name, *, rv=None, file_type=None, precision=None,
-              write_patch_results=False):
+              write_patch_results=False, write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         - If rv is None, the simple correlation function :math:`\langle v_R\rangle` is used.
@@ -486,13 +487,13 @@ def write(self, file_name, *, rv=None, file_type=None, precision=None,
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing NV correlations to %s',file_name)
         self.calculateXi(rv=rv)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -507,14 +508,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create an NVCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: An NVCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building NVCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading NV correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/qqcorrelation.py b/treecorr/qqcorrelation.py
index 007743ec..2a4833b9 100644
--- a/treecorr/qqcorrelation.py
+++ b/treecorr/qqcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarQ
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class QQCorrelation(Corr2):
@@ -174,7 +175,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'QQCorrelation(config=%r)'%self.config
+        return f'QQCorrelation({self._repr_kwargs})'
 
     def process_auto(self, cat, *, metric=None, num_threads=None):
         """Process a single catalog, accumulating the auto-correlation.
@@ -442,7 +443,8 @@ def process(self, cat1, cat2=None, *, metric=None, num_threads=None, comm=None,
             self._processed_cats1.clear()
             self._processed_cats2.clear()
 
-    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False):
+    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False,
+              write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         The output file will include the following columns:
@@ -477,12 +479,12 @@ def write(self, file_name, *, file_type=None, precision=None, write_patch_result
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing QQ correlations to %s',file_name)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -500,14 +502,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create a QQCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: A QQCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building QQCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading QQ correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/reader.py b/treecorr/reader.py
index 1e07218a..ad00a7ca 100644
--- a/treecorr/reader.py
+++ b/treecorr/reader.py
@@ -34,11 +34,11 @@ class AsciiReader(object):
     can_slice = True
     default_ext = None
 
-    def __init__(self, file_name, delimiter=None, comment_marker='#', logger=None):
+    def __init__(self, file_name, *, delimiter=None, comment_marker='#', logger=None):
         """
         Parameters:
-            file_name (str):        The file name
-            delimiter (str):        What delimiter to use between values.  (default: None,
+            file_name (str):        The file name.
+            delimiter (str):        What delimiter to use between values. (default: None,
                                     which means any whitespace)
             comment_marker (str):   What token indicates a comment line. (default: '#')
         """
@@ -60,7 +60,7 @@ def __contains__(self, ext):
         ASCII files don't have extensions, so the only ext allowed is None.
 
         Parameters:
-            ext (str):      The extension to check
+            ext (str):      The extension to check.
 
         Returns:
             Whether ext is None
@@ -74,19 +74,19 @@ def check_valid_ext(self, ext):
         None is the only valid extension for ASCII files.
 
         Parameters:
-            ext (str):  The extension to check
+            ext (str):  The extension to check.
         """
         if ext is not None:
             raise ValueError("Invalid ext={} for file {}".format(ext,self.file_name))
 
 
-    def read(self, cols, s=slice(None), ext=None):
+    def read(self, cols, s=slice(None), *, ext=None):
         """Read a slice of a column or list of columns from a specified extension.
 
         Parameters:
-            cols (str/list):    The name(s) of column(s) to read
-            s (slice/array):    A slice object or selection of integers to read (default: all)
-            ext (str):          The extension (ignored)
+            cols (str/list):    The name(s) of column(s) to read.
+            s (slice/array):    A slice object or selection of integers to read. (default: all)
+            ext (str):          The extension. (ignored)
 
         Returns:
             The data as a dict or single numpy array as appropriate
@@ -140,11 +140,11 @@ def read(self, cols, s=slice(None), ext=None):
         else:
             return {col : data[:,i] for i,col in enumerate(cols)}
 
-    def read_params(self, ext=None):
+    def read_params(self, *, ext=None):
         """Read the params in the given extension, if any.
 
         Parameters:
-            ext (str):          The extension (ignored -- Ascii always reads the next group)
+            ext (str):          The extension. (ignored -- Ascii always reads the next group)
 
         Returns:
             params
@@ -169,11 +169,11 @@ def read_params(self, ext=None):
         self.ncols = len(self.col_names)
         return params
 
-    def read_data(self, ext=None, max_rows=None):
+    def read_data(self, *, ext=None, max_rows=None):
         """Read all data in the file, and the parameters in the header, if any.
 
         Parameters:
-            ext (str):          The extension (ignored -- Ascii always reads the next group)
+            ext (str):          The extension. (ignored -- Ascii always reads the next group)
             max_rows (int):     The max number of rows to read. (default: None)
 
         Returns:
@@ -182,12 +182,26 @@ def read_data(self, ext=None, max_rows=None):
         data = np.genfromtxt(self.file, names=self.col_names, max_rows=max_rows)
         return data
 
-    def row_count(self, col=None, ext=None):
+    def read_array(self, shape, *, ext=None):
+        """Read an array from the file.
+
+        Parameters:
+            shape (tuple):      The expected shape of the array.
+            ext (str):          The extension. (ignored -- Ascii always reads the next group)
+
+        Returns:
+            array
+        """
+        data = np.genfromtxt(self.file, max_rows=shape[0])
+        assert data.shape == shape
+        return data
+
+    def row_count(self, col=None, *, ext=None):
         """Count the number of rows in the file.
 
         Parameters:
-            col (str):  The column to use (ignored)
-            ext (str):  The extension (ignored)
+            col (str):  The column to use. (ignored)
+            ext (str):  The extension. (ignored)
 
         Returns:
             The number of rows
@@ -210,11 +224,11 @@ def row_count(self, col=None, ext=None):
         self.nrows = lines - self.comment_rows
         return self.nrows
 
-    def names(self, ext=None):
+    def names(self, *, ext=None):
         """Return a list of the names of all the columns in an extension
 
         Parameters:
-            ext (str):  The extension (ignored)
+            ext (str):  The extension. (ignored)
 
         Returns:
             A list of string column names
@@ -262,11 +276,11 @@ def __exit__(self, exc_type, exc_value, exc_traceback):
 class PandasReader(AsciiReader):
     """Reader interface for ASCII files using pandas.
     """
-    def __init__(self, file_name, delimiter=None, comment_marker='#', logger=None):
+    def __init__(self, file_name, *, delimiter=None, comment_marker='#', logger=None):
         """
         Parameters:
-            file_name (str):        The file name
-            delimiter (str):        What delimiter to use between values.  (default: None,
+            file_name (str):        The file name.
+            delimiter (str):        What delimiter to use between values. (default: None,
                                     which means any whitespace)
             comment_marker (str):   What token indicates a comment line. (default: '#')
         """
@@ -277,18 +291,18 @@ def __init__(self, file_name, delimiter=None, comment_marker='#', logger=None):
                 logger.error("Unable to import pandas.  Cannot read %s"%file_name)
             raise
 
-        AsciiReader.__init__(self, file_name, delimiter, comment_marker)
+        AsciiReader.__init__(self, file_name, delimiter=delimiter, comment_marker=comment_marker)
         # This is how pandas handles whitespace
         self.sep = r'\s+' if self.delimiter is None else self.delimiter
 
 
-    def read(self, cols, s=slice(None), ext=None):
+    def read(self, cols, s=slice(None), *, ext=None):
         """Read a slice of a column or list of columns from a specified extension.
 
         Parameters:
-            cols (str/list):    The name(s) of column(s) to read
-            s (slice/array):    A slice object or selection of integers to read (default: all)
-            ext (str):          The extension (ignored)
+            cols (str/list):    The name(s) of column(s) to read.
+            s (slice/array):    A slice object or selection of integers to read. (default: all)
+            ext (str):          The extension. (ignored)
 
         Returns:
             The data as a dict or single numpy array as appropriate
@@ -349,11 +363,11 @@ class ParquetReader():
     can_slice = True
     default_ext = None
 
-    def __init__(self, file_name, delimiter=None, comment_marker='#', logger=None):
+    def __init__(self, file_name, *, delimiter=None, comment_marker='#', logger=None):
         """
         Parameters:
-            file_name (str):        The file name
-            delimiter (str):        What delimiter to use between values.  (default: None,
+            file_name (str):        The file name.
+            delimiter (str):        What delimiter to use between values. (default: None,
                                     which means any whitespace)
             comment_marker (str):   What token indicates a comment line. (default: '#')
         """
@@ -379,7 +393,7 @@ def __contains__(self, ext):
         Parquet files don't have extensions, so the only ext allowed is None.
 
         Parameters:
-            ext (str):      The extension to check
+            ext (str):      The extension to check.
 
         Returns:
             Whether ext is None
@@ -393,18 +407,18 @@ def check_valid_ext(self, ext):
         None is the only valid extension for ASCII files.
 
         Parameters:
-            ext (str):  The extension to check
+            ext (str):  The extension to check.
         """
         if ext is not None:
             raise ValueError("Invalid ext={} for file {}".format(ext,self.file_name))
 
-    def read(self, cols, s=slice(None), ext=None):
+    def read(self, cols, s=slice(None), *, ext=None):
         """Read a slice of a column or list of columns from a specified extension.
 
         Parameters:
-            cols (str/list):    The name(s) of column(s) to read
-            s (slice/array):    A slice object or selection of integers to read (default: all)
-            ext (str):          The extension (ignored)
+            cols (str/list):    The name(s) of column(s) to read.
+            s (slice/array):    A slice object or selection of integers to read. (default: all)
+            ext (str):          The extension. (ignored)
 
         Returns:
             The data as a recarray or simple numpy array as appropriate
@@ -414,25 +428,25 @@ def read(self, cols, s=slice(None), ext=None):
         else:
             return self.df[cols][s].to_records()
 
-    def row_count(self, col=None, ext=None):
+    def row_count(self, col=None, *, ext=None):
         """Count the number of rows in the named extension and column
 
         Unlike in FitsReader, col is required.
 
         Parameters:
-            col (str):  The column to use (ignored)
-            ext (str):  The extension (ignored)
+            col (str):  The column to use. (ignored)
+            ext (str):  The extension. (ignored)
 
         Returns:
             The number of rows
         """
         return len(self.df)
 
-    def names(self, ext=None):
+    def names(self, *, ext=None):
         """Return a list of the names of all the columns in an extension
 
         Parameters:
-            ext (str):  The extension to search for columns (ignored)
+            ext (str):  The extension to search for columns. (ignored)
 
         Returns:
             A list of string column names
@@ -455,10 +469,10 @@ class FitsReader(object):
     """
     default_ext = 1
 
-    def __init__(self, file_name, logger=None):
+    def __init__(self, file_name, *, logger=None):
         """
         Parameters:
-            file_name (str):    The file name
+            file_name (str):    The file name.
         """
         try:
             import fitsio
@@ -487,7 +501,7 @@ def __contains__(self, ext):
         This may be either a name or an integer.
 
         Parameters:
-            ext (str/int):  The extension to check for (default: 1)
+            ext (str/int):  The extension to check for. (default: 1)
 
         Returns:
             Whether the extension exists
@@ -501,7 +515,7 @@ def check_valid_ext(self, ext):
         The ext must both exist and be a table (not an image)
 
         Parameters:
-            ext (str/int):  The extension to check
+            ext (str/int):  The extension to check.
         """
         import fitsio
 
@@ -514,12 +528,12 @@ def check_valid_ext(self, ext):
             raise ValueError("Invalid ext={} for file {} (Not a TableHDU)".format(
                              ext, self.file_name))
 
-    def _update_ext(self, ext):
+    def _update_ext(self, ext, default=1):
         # FITS extensions can be indexed by number or
         # string.  Try converting to an integer if the current
         # value is not found.  If not let the error be caught later.
         if ext is None:
-            ext = 1
+            ext = default
         if ext not in self.file:
             try:
                 ext = int(ext)
@@ -527,13 +541,13 @@ def _update_ext(self, ext):
                 pass
         return ext
 
-    def read(self, cols, s=slice(None), ext=None):
+    def read(self, cols, s=slice(None), *, ext=None):
         """Read a slice of a column or list of columns from a specified extension
 
         Parameters:
-            cols (str/list):    The name(s) of column(s) to read
-            s (slice/array):    A slice object or selection of integers to read (default: all)
-            ext (str/int)):     The FITS extension to use (default: 1)
+            cols (str/list):    The name(s) of column(s) to read.
+            s (slice/array):    A slice object or selection of integers to read. (default: all)
+            ext (str/int)):     The FITS extension to use. (default: 1)
 
         Returns:
             The data as a recarray or simple numpy array as appropriate
@@ -541,24 +555,25 @@ def read(self, cols, s=slice(None), ext=None):
         ext = self._update_ext(ext)
         return self.file[ext][cols][s]
 
-    def read_params(self, ext=None):
+    def read_params(self, *, ext=None):
         """Read the params in the given extension, if any.
 
         Parameters:
-            ext (str/int):      The FITS extension to use (default: 1)
+            ext (str/int):      The FITS extension to use. (default: 1)
 
         Returns:
             params
         """
         ext = self._update_ext(ext)
         params = self.file[ext].read_header()
-        return params
+        # Convert to dict for convience downstream, and make all keys lowercase.
+        return { k.lower() : params[k] for k in params }
 
-    def read_data(self, ext=None, max_rows=None):
+    def read_data(self, *, ext=None, max_rows=None):
         """Read all data in the file, and the parameters in the header, if any.
 
         Parameters:
-            ext (str/int):      The FITS extension to use (default: 1)
+            ext (str/int):      The FITS extension to use. (default: 1)
             max_rows (int):     The max number of rows to read. (ignored)
 
         Returns:
@@ -568,7 +583,22 @@ def read_data(self, ext=None, max_rows=None):
         data = self.file[ext].read()
         return data
 
-    def row_count(self, col=None, ext=None):
+    def read_array(self, shape, *, ext=None):
+        """Read an array from the file.
+
+        Parameters:
+            shape (tuple):      The expected shape of the array.
+            ext (str):          The extension. (ignored -- Ascii always reads the next group)
+
+        Returns:
+            array
+        """
+        ext = self._update_ext(ext, 0)
+        data = self.file[ext].read()
+        assert data.shape == shape
+        return data
+
+    def row_count(self, col=None, *, ext=None):
         """Count the number of rows in the named extension
 
         For compatibility with the HDF interface, which can have columns
@@ -576,8 +606,8 @@ def row_count(self, col=None, ext=None):
         ignored here.
 
         Parameters:
-            col (str):      The column to use (ignored)
-            ext (str/int):  The FITS extension to use (default: 1)
+            col (str):      The column to use. (ignored)
+            ext (str/int):  The FITS extension to use. (default: 1)
 
         Returns:
             The number of rows
@@ -585,11 +615,11 @@ def row_count(self, col=None, ext=None):
         ext = self._update_ext(ext)
         return self.file[ext].get_nrows()
 
-    def names(self, ext=None):
+    def names(self, *, ext=None):
         """Return a list of the names of all the columns in an extension
 
         Parameters:
-            ext (str/int):  The extension to search for columns (default: 1)
+            ext (str/int):  The extension to search for columns. (default: 1)
 
         Returns:
             A list of string column names
@@ -617,10 +647,10 @@ class HdfReader(object):
     can_slice = True
     default_ext = '/'
 
-    def __init__(self, file_name, logger=None):
+    def __init__(self, file_name, *, logger=None):
         """
         Parameters:
-            file_name (str):    The file name
+            file_name (str):    The file name.
         """
         try:
             import h5py
@@ -642,7 +672,7 @@ def __contains__(self, ext):
         """Check if there is an extension with the given name in the file.
 
         Parameters:
-            ext (str):      The extension to check for
+            ext (str):      The extension to check for.
 
         Returns:
             Whether the extension exists
@@ -663,22 +693,22 @@ def check_valid_ext(self, ext):
         The ext must exist - there is no other requirement for HDF files.
 
         Parameters:
-            ext (str):  The extension to check
+            ext (str):  The extension to check.
         """
         if ext not in self:
             raise ValueError("Invalid ext={} for file {} (does not exist)".format(
                              ext,self.file_name))
 
-    def read(self, cols, s=slice(None), ext=None):
+    def read(self, cols, s=slice(None), *, ext=None):
         """Read a slice of a column or list of columns from a specified extension.
 
         Slices should always be used when reading HDF files - using a sequence of
         integers is painfully slow.
 
         Parameters:
-            cols (str/list):    The name(s) of column(s) to read
-            s (slice/array):    A slice object or selection of integers to read (default: all)
-            ext (str):          The HDF (sub-)group to use (default: '/')
+            cols (str/list):    The name(s) of column(s) to read.
+            s (slice/array):    A slice object or selection of integers to read. (default: all)
+            ext (str):          The HDF (sub-)group to use. (default: '/')
 
         Returns:
             The data as a dict or single numpy array as appropriate
@@ -689,11 +719,11 @@ def read(self, cols, s=slice(None), ext=None):
         else:
             return {col : g[col][s] for col in cols}
 
-    def read_params(self, ext=None):
+    def read_params(self, *, ext=None):
         """Read the params in the given extension, if any.
 
         Parameters:
-            ext (str):          The HDF (sub-)group to use (default: '/')
+            ext (str):          The HDF (sub-)group to use. (default: '/')
 
         Returns:
             params
@@ -703,11 +733,11 @@ def read_params(self, ext=None):
 
         return params
 
-    def read_data(self, ext=None, max_rows=None):
+    def read_data(self, *, ext=None, max_rows=None):
         """Read all data in the file, and the parameters in the attributes, if any.
 
         Parameters:
-            ext (str):          The HDF (sub-)group to use (default: '/')
+            ext (str):          The HDF (sub-)group to use. (default: '/')
             max_rows (int):     The max number of rows to read. (ignored)
 
         Returns:
@@ -730,25 +760,40 @@ def read_data(self, ext=None, max_rows=None):
 
         return data
 
-    def row_count(self, col, ext=None):
+    def read_array(self, shape, *, ext=None):
+        """Read an array from the file.
+
+        Parameters:
+            shape (tuple):      The expected shape of the array.
+            ext (str):          The extension. (ignored -- Ascii always reads the next group)
+
+        Returns:
+            array
+        """
+        g = self._group(ext)
+        data = np.empty(shape)
+        data[:] = g['data']
+        return data
+
+    def row_count(self, col, *, ext=None):
         """Count the number of rows in the named extension and column
 
         Unlike in FitsReader, col is required.
 
         Parameters:
-            col (str):  The column to use
-            ext (str):  The HDF group name to use (default: '/')
+            col (str):  The column to use.
+            ext (str):  The HDF group name to use. (default: '/')
 
         Returns:
             The number of rows
         """
         return self._group(ext)[col].size
 
-    def names(self, ext=None):
+    def names(self, *, ext=None):
         """Return a list of the names of all the columns in an extension
 
         Parameters:
-            ext (str):  The extension to search for columns (default: '/')
+            ext (str):  The extension to search for columns. (default: '/')
 
         Returns:
             A list of string column names
diff --git a/treecorr/ttcorrelation.py b/treecorr/ttcorrelation.py
index 0be0bf78..356bc251 100644
--- a/treecorr/ttcorrelation.py
+++ b/treecorr/ttcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarT
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class TTCorrelation(Corr2):
@@ -174,7 +175,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'TTCorrelation(config=%r)'%self.config
+        return f'TTCorrelation({self._repr_kwargs})'
 
     def process_auto(self, cat, *, metric=None, num_threads=None):
         """Process a single catalog, accumulating the auto-correlation.
@@ -441,7 +442,8 @@ def process(self, cat1, cat2=None, *, metric=None, num_threads=None, comm=None,
             self._processed_cats1.clear()
             self._processed_cats2.clear()
 
-    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False):
+    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False,
+              write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         The output file will include the following columns:
@@ -476,12 +478,12 @@ def write(self, file_name, *, file_type=None, precision=None, write_patch_result
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing TT correlations to %s',file_name)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -499,14 +501,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create a TTCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: A TTCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building TTCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading TT correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/util.py b/treecorr/util.py
index 9db77a53..b1ba2807 100644
--- a/treecorr/util.py
+++ b/treecorr/util.py
@@ -109,7 +109,7 @@ def parse_file_type(file_type, file_name, output=False, logger=None):
 
     :param file_type:   The input file_type.  If None, then parse from file_name's extension.
     :param file_name:   The filename to use for parsing if necessary.
-    :param output:      Limit to output file types (FITS/ASCII)? (default: False)
+    :param output:      Limit to output file types (FITS/HDF/ASCII)? (default: False)
     :param logger:      A logger if desired. (default: None)
 
     :returns: The parsed file_type.
diff --git a/treecorr/vvcorrelation.py b/treecorr/vvcorrelation.py
index ae7911dd..24e3406f 100644
--- a/treecorr/vvcorrelation.py
+++ b/treecorr/vvcorrelation.py
@@ -21,6 +21,7 @@
 from .catalog import calculateVarV
 from .corr2base import Corr2
 from .util import make_writer, make_reader
+from .config import make_minimal_config
 
 
 class VVCorrelation(Corr2):
@@ -174,7 +175,7 @@ def copy(self):
         return ret
 
     def __repr__(self):
-        return 'VVCorrelation(config=%r)'%self.config
+        return f'VVCorrelation({self._repr_kwargs})'
 
     def process_auto(self, cat, *, metric=None, num_threads=None):
         """Process a single catalog, accumulating the auto-correlation.
@@ -441,7 +442,8 @@ def process(self, cat1, cat2=None, *, metric=None, num_threads=None, comm=None,
             self._processed_cats1.clear()
             self._processed_cats2.clear()
 
-    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False):
+    def write(self, file_name, *, file_type=None, precision=None, write_patch_results=False,
+              write_cov=False):
         r"""Write the correlation function to the file, file_name.
 
         The output file will include the following columns:
@@ -476,12 +478,12 @@ def write(self, file_name, *, file_type=None, precision=None, write_patch_result
                                 this value can also be given in the constructor in the config dict.)
             write_patch_results (bool): Whether to write the patch-based results as well.
                                         (default: False)
+            write_cov (bool):   Whether to write the covariance matrix as well. (default: False)
         """
         self.logger.info('Writing VV correlations to %s',file_name)
         precision = self.config.get('precision', 4) if precision is None else precision
-        name = 'main' if write_patch_results else None
         with make_writer(file_name, precision, file_type, self.logger) as writer:
-            self._write(writer, name, write_patch_results)
+            self._write(writer, None, write_patch_results, write_cov=write_cov)
 
     @property
     def _write_col_names(self):
@@ -499,14 +501,45 @@ def _write_data(self):
 
     @property
     def _write_params(self):
-        return { 'coords' : self.coords, 'metric' : self.metric,
-                 'sep_units' : self.sep_units, 'bin_type' : self.bin_type }
+        params = make_minimal_config(self.config, Corr2._valid_params)
+        # Add in a couple other things we want to preserve that aren't construction kwargs.
+        params['coords'] = self.coords
+        params['metric'] = self.metric
+        return params
+
+    @classmethod
+    def from_file(cls, file_name, *, file_type=None, logger=None, rng=None):
+        """Create a VVCorrelation instance from an output file.
+
+        This should be a file that was written by TreeCorr.
+
+        Parameters:
+            file_name (str):    The name of the file to read in.
+            file_type (str):    The type of file ('ASCII', 'FITS', or 'HDF').  (default: determine
+                                the type automatically from the extension of file_name.)
+            logger (Logger):    If desired, a logger object to use for logging. (default: None)
+            rng (RandomState):  If desired, a numpy.random.RandomState instance to use for bootstrap
+                                random number generation. (default: None)
+
+        Returns:
+            corr: A VVCorrelation object, constructed from the information in the file.
+        """
+        if logger:
+            logger.info('Building VVCorrelation from %s',file_name)
+        with make_reader(file_name, file_type, logger) as reader:
+            name = 'main' if 'main' in reader else None
+            params = reader.read_params(ext=name)
+            kwargs = make_minimal_config(params, Corr2._valid_params)
+            corr = cls(**kwargs, logger=logger, rng=rng)
+            corr.logger.info('Reading VV correlations from %s',file_name)
+            corr._read(reader, name=name, params=params)
+        return corr
 
     def read(self, file_name, *, file_type=None):
         """Read in values from a file.
 
-        This should be a file that was written by TreeCorr, preferably a FITS file, so there
-        is no loss of information.
+        This should be a file that was written by TreeCorr, preferably a FITS or HDF5 file, so
+        there is no loss of information.
 
         .. warning::
 
diff --git a/treecorr/writer.py b/treecorr/writer.py
index a28df15e..a1bb259d 100644
--- a/treecorr/writer.py
+++ b/treecorr/writer.py
@@ -26,7 +26,7 @@ class AsciiWriter(object):
     def __init__(self, file_name, *, precision=4, logger=None):
         """
         Parameters:
-            file_name:      The file name
+            file_name:      The file name.
             precision:      The number of digits of precision to output.
             logger:         If desired, a logger object for logging. (default: None)
         """
@@ -51,7 +51,7 @@ def write(self, col_names, columns, *, params=None, ext=None):
         """Write some columns to an output ASCII file with the given column names.
 
         Parameters:
-            col_names:      A list of columns names for the given columns.  These will be written
+            col_names:      A list of columns names for the given columns. These will be written
                             in a header comment line at the top of the output file.
             columns:        A list of numpy arrays with the data to write.
             params:         A dict of extra parameters to write at the top of the output file.
@@ -75,6 +75,18 @@ def write(self, col_names, columns, *, params=None, ext=None):
         self.file.write(header.encode())
         np.savetxt(self.file, data, fmt=self.fmt)
 
+    def write_array(self, data, *, ext=None):
+        """Write a (typically 2-d) numpy array to the output file.
+
+        Parameters:
+            data:           The array to write.
+            ext:            Optional ext name for these data. (default: None)
+        """
+        if ext is not None:
+            s = '## %s\n'%ext
+            self.file.write(s.encode())
+        np.savetxt(self.file, data, fmt=self.fmt)
+
     def __enter__(self):
         self._file = open(self.file_name, 'wb')
         return self
@@ -90,7 +102,7 @@ class FitsWriter(object):
     def __init__(self, file_name, *, logger=None):
         """
         Parameters:
-            file_name:      The file name
+            file_name:      The file name.
             logger:         If desired, a logger object for logging. (default: None)
         """
         try:
@@ -129,6 +141,15 @@ def write(self, col_names, columns, *, params=None, ext=None):
             data[c] = col
         self.file.write(data, header=params, extname=ext)
 
+    def write_array(self, data, *, ext=None):
+        """Write a (typically 2-d) numpy array to the output file.
+
+        Parameters:
+            data:           The array to write.
+            ext:            Optional ext name for these data. (default: None)
+        """
+        self.file.write(data, extname=ext)
+
     def __enter__(self):
         import fitsio
         self._file = fitsio.FITS(self.file_name, 'rw', clobber=True)
@@ -146,7 +167,7 @@ class HdfWriter(object):
     def __init__(self, file_name, *, logger=None):
         """
         Parameters:
-            file_name:     The file name
+            file_name:      The file name.
             logger:         If desired, a logger object for logging. (default: None)
         """
         try:
@@ -189,6 +210,19 @@ def write(self, col_names, columns, *, params=None, ext=None):
         for (name, col) in zip(col_names, columns):
             hdf.create_dataset(name, data=col)
 
+    def write_array(self, data, *, ext=None):
+        """Write a (typically 2-d) numpy array to the output file.
+
+        Parameters:
+            data:           The array to write.
+            ext:            Optional ext name for these data. (default: None)
+        """
+        if ext is not None:
+            hdf = self.file.create_group(ext)
+        else:
+            hdf = self.file
+        hdf.create_dataset('data', data=data)
+
     def __enter__(self):
         import h5py
         self._file = h5py.File(self.file_name, 'w')


Test patch:
diff --git a/tests/Tutorial.ipynb b/tests/Tutorial.ipynb
index ba66bda7..c1e71293 100644
--- a/tests/Tutorial.ipynb
+++ b/tests/Tutorial.ipynb
@@ -252,7 +252,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "## {'coords': 'spherical', 'metric': 'Euclidean', 'sep_units': 'arcmin', 'bin_type': 'Log'}\n",
+      "## {'min_sep': 1.0, 'max_sep': 400.0, 'nbins': 100, 'sep_units': 'arcmin', 'verbose': 2, 'output_dots': True, 'coords': 'spherical', 'metric': 'Euclidean'}\n",
       "#   r_nom       meanr       meanlogr       xip          xim         xip_im       xim_im     sigma_xip    sigma_xim      weight       npairs   \n",
       "  1.0304e+00   1.0311e+00   3.0480e-02   7.5773e-06   1.6730e-07   7.0287e-08  -3.5157e-08   1.8983e-08   1.8983e-08   2.0181e+05   2.0181e+05\n",
       "  1.0940e+00   1.0948e+00   9.0389e-02   7.3854e-06   3.5900e-07  -2.0456e-08   1.5109e-08   1.7939e-08   1.7939e-08   2.2597e+05   2.2597e+05\n",
diff --git a/tests/mpi_test.py b/tests/mpi_test.py
index 5ef6a479..ad40b49d 100644
--- a/tests/mpi_test.py
+++ b/tests/mpi_test.py
@@ -344,14 +344,10 @@ def do_mpi_cov(comm, method, output=True):
 
     # Finally, read back in from the save file and redo the covariance.
     rng = np.random.default_rng(31415)
-    gg = treecorr.GGCorrelation(bin_size=0.3, min_sep=10., max_sep=50., rng=rng)
-    ng = treecorr.NGCorrelation(bin_size=0.3, min_sep=10., max_sep=50., rng=rng)
-    nn = treecorr.NNCorrelation(bin_size=0.3, min_sep=10., max_sep=50., rng=rng)
-    rr = treecorr.NNCorrelation(bin_size=0.3, min_sep=10., max_sep=50., rng=rng)
-    gg.read(gg_save_file)
-    ng.read(ng_save_file)
-    nn.read(nn_save_file)
-    rr.read(rr_save_file)
+    gg = treecorr.GGCorrelation.from_file(gg_save_file, rng=rng)
+    ng = treecorr.NGCorrelation.from_file(ng_save_file, rng=rng)
+    nn = treecorr.NNCorrelation.from_file(nn_save_file, rng=rng)
+    rr = treecorr.NNCorrelation.from_file(rr_save_file, rng=rng)
 
     ng.calculateXi()
     nn.calculateXi(rr=rr)
diff --git a/tests/test_gg.py b/tests/test_gg.py
index 7a719168..ff401386 100644
--- a/tests/test_gg.py
+++ b/tests/test_gg.py
@@ -154,7 +154,7 @@ def test_direct():
 
     ascii_name = 'output/gg_ascii.txt'
     gg.write(ascii_name, precision=16)
-    gg3 = treecorr.GGCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    gg3 = treecorr.GGCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     gg3.read(ascii_name)
     np.testing.assert_allclose(gg3.npairs, gg.npairs)
     np.testing.assert_allclose(gg3.weight, gg.weight)
@@ -165,6 +165,22 @@ def test_direct():
     np.testing.assert_allclose(gg3.xim, gg.xim)
     np.testing.assert_allclose(gg3.xim_im, gg.xim_im)
 
+    # Check that the repr is minimal
+    assert repr(gg3) == f'GGCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # New in version 5.0 is a simpler API for reading
+    with CaptureLog() as cl:
+        gg3b = treecorr.GGCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(gg3b.npairs, gg.npairs)
+    np.testing.assert_allclose(gg3b.weight, gg.weight)
+    np.testing.assert_allclose(gg3b.meanr, gg.meanr)
+    np.testing.assert_allclose(gg3b.meanlogr, gg.meanlogr)
+    np.testing.assert_allclose(gg3b.xip, gg.xip)
+    np.testing.assert_allclose(gg3b.xip_im, gg.xip_im)
+    np.testing.assert_allclose(gg3b.xim, gg.xim)
+    np.testing.assert_allclose(gg3b.xim_im, gg.xim_im)
+
     try:
         import fitsio
     except ImportError:
@@ -183,6 +199,16 @@ def test_direct():
         np.testing.assert_allclose(gg4.xim, gg.xim)
         np.testing.assert_allclose(gg4.xim_im, gg.xim_im)
 
+        gg4b = treecorr.GGCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(gg4b.npairs, gg.npairs)
+        np.testing.assert_allclose(gg4b.weight, gg.weight)
+        np.testing.assert_allclose(gg4b.meanr, gg.meanr)
+        np.testing.assert_allclose(gg4b.meanlogr, gg.meanlogr)
+        np.testing.assert_allclose(gg4b.xip, gg.xip)
+        np.testing.assert_allclose(gg4b.xip_im, gg.xip_im)
+        np.testing.assert_allclose(gg4b.xim, gg.xim)
+        np.testing.assert_allclose(gg4b.xim_im, gg.xim_im)
+
     with assert_raises(TypeError):
         gg2 += config
     gg4 = treecorr.GGCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -566,8 +592,7 @@ def test_gg():
     np.testing.assert_allclose(data['npairs'], gg.npairs)
 
     # Check the read function
-    gg2 = treecorr.GGCorrelation(bin_size=0.1, min_sep=1., max_sep=100., sep_units='arcmin')
-    gg2.read(out_file_name)
+    gg2 = treecorr.GGCorrelation.from_file(out_file_name)
     np.testing.assert_allclose(gg2.logr, gg.logr)
     np.testing.assert_allclose(gg2.meanr, gg.meanr)
     np.testing.assert_allclose(gg2.meanlogr, gg.meanlogr)
diff --git a/tests/test_ggg.py b/tests/test_ggg.py
index 43f13168..c4ff76e6 100644
--- a/tests/test_ggg.py
+++ b/tests/test_ggg.py
@@ -18,7 +18,7 @@
 import time
 
 from test_helper import do_pickle, assert_raises, timer, is_ccw, is_ccw_3d
-from test_helper import get_from_wiki
+from test_helper import get_from_wiki, CaptureLog
 
 @timer
 def test_direct_logruv():
@@ -336,6 +336,28 @@ def test_direct_logruv():
     np.testing.assert_allclose(ggg3.gam3r, ggg.gam3r)
     np.testing.assert_allclose(ggg3.gam3i, ggg.gam3i)
 
+    # New in version 5.0 is a simpler API for reading
+    with CaptureLog() as cl:
+        ggg3b = treecorr.GGGCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(ggg3b.weight, ggg.weight)
+    np.testing.assert_allclose(ggg3b.meand1, ggg.meand1)
+    np.testing.assert_allclose(ggg3b.meand2, ggg.meand2)
+    np.testing.assert_allclose(ggg3b.meand3, ggg.meand3)
+    np.testing.assert_allclose(ggg3b.meanlogd1, ggg.meanlogd1)
+    np.testing.assert_allclose(ggg3b.meanlogd2, ggg.meanlogd2)
+    np.testing.assert_allclose(ggg3b.meanlogd3, ggg.meanlogd3)
+    np.testing.assert_allclose(ggg3b.meanu, ggg.meanu)
+    np.testing.assert_allclose(ggg3b.meanv, ggg.meanv)
+    np.testing.assert_allclose(ggg3b.gam0r, ggg.gam0r)
+    np.testing.assert_allclose(ggg3b.gam0i, ggg.gam0i)
+    np.testing.assert_allclose(ggg3b.gam1r, ggg.gam1r)
+    np.testing.assert_allclose(ggg3b.gam1i, ggg.gam1i)
+    np.testing.assert_allclose(ggg3b.gam2r, ggg.gam2r)
+    np.testing.assert_allclose(ggg3b.gam2i, ggg.gam2i)
+    np.testing.assert_allclose(ggg3b.gam3r, ggg.gam3r)
+    np.testing.assert_allclose(ggg3b.gam3i, ggg.gam3i)
+
     try:
         import fitsio
     except ImportError:
@@ -365,6 +387,25 @@ def test_direct_logruv():
         np.testing.assert_allclose(ggg4.gam3r, ggg.gam3r)
         np.testing.assert_allclose(ggg4.gam3i, ggg.gam3i)
 
+        ggg4b = treecorr.GGGCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(ggg4b.weight, ggg.weight)
+        np.testing.assert_allclose(ggg4b.meand1, ggg.meand1)
+        np.testing.assert_allclose(ggg4b.meand2, ggg.meand2)
+        np.testing.assert_allclose(ggg4b.meand3, ggg.meand3)
+        np.testing.assert_allclose(ggg4b.meanlogd1, ggg.meanlogd1)
+        np.testing.assert_allclose(ggg4b.meanlogd2, ggg.meanlogd2)
+        np.testing.assert_allclose(ggg4b.meanlogd3, ggg.meanlogd3)
+        np.testing.assert_allclose(ggg4b.meanu, ggg.meanu)
+        np.testing.assert_allclose(ggg4b.meanv, ggg.meanv)
+        np.testing.assert_allclose(ggg4b.gam0r, ggg.gam0r)
+        np.testing.assert_allclose(ggg4b.gam0i, ggg.gam0i)
+        np.testing.assert_allclose(ggg4b.gam1r, ggg.gam1r)
+        np.testing.assert_allclose(ggg4b.gam1i, ggg.gam1i)
+        np.testing.assert_allclose(ggg4b.gam2r, ggg.gam2r)
+        np.testing.assert_allclose(ggg4b.gam2i, ggg.gam2i)
+        np.testing.assert_allclose(ggg4b.gam3r, ggg.gam3r)
+        np.testing.assert_allclose(ggg4b.gam3i, ggg.gam3i)
+
     assert ggg.var_method == 'shot'  # Only option currently.
 
     with assert_raises(TypeError):
@@ -1602,11 +1643,7 @@ def test_ggg_logruv():
         # Check the read function
         # Note: These don't need the flatten.
         # The read function should reshape them to the right shape.
-        ggg2 = treecorr.GGGCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins,
-                                   min_u=min_u, max_u=max_u, min_v=min_v, max_v=max_v,
-                                   nubins=nubins, nvbins=nvbins,
-                                   sep_units='arcmin', verbose=1, bin_type='LogRUV')
-        ggg2.read(out_file_name1)
+        ggg2 = treecorr.GGGCorrelation.from_file(out_file_name1)
         np.testing.assert_allclose(ggg2.logr, ggg.logr)
         np.testing.assert_allclose(ggg2.u, ggg.u)
         np.testing.assert_allclose(ggg2.v, ggg.v)
@@ -2534,6 +2571,23 @@ def test_direct_logsas():
     np.testing.assert_allclose(ggg3.gam2, ggg.gam2)
     np.testing.assert_allclose(ggg3.gam3, ggg.gam3)
 
+    # Check that the repr is minimal
+    assert repr(ggg3) == f"GGGCorrelation(min_sep={min_sep}, bin_size={bin_size}, nbins={nbins}, nphi_bins={nphi_bins})"
+
+    # New in version 5.0 is a simpler API for reading
+    ggg3b = treecorr.GGGCorrelation.from_file(ascii_name)
+    np.testing.assert_allclose(ggg3b.weight, ggg.weight)
+    np.testing.assert_allclose(ggg3b.meand1, ggg.meand1)
+    np.testing.assert_allclose(ggg3b.meand2, ggg.meand2)
+    np.testing.assert_allclose(ggg3b.meand3, ggg.meand3)
+    np.testing.assert_allclose(ggg3b.meanlogd1, ggg.meanlogd1)
+    np.testing.assert_allclose(ggg3b.meanlogd2, ggg.meanlogd2)
+    np.testing.assert_allclose(ggg3b.meanlogd3, ggg.meanlogd3)
+    np.testing.assert_allclose(ggg3b.gam0, ggg.gam0)
+    np.testing.assert_allclose(ggg3b.gam1, ggg.gam1)
+    np.testing.assert_allclose(ggg3b.gam2, ggg.gam2)
+    np.testing.assert_allclose(ggg3b.gam3, ggg.gam3)
+
     try:
         import fitsio
     except ImportError:
@@ -2558,6 +2612,19 @@ def test_direct_logsas():
         np.testing.assert_allclose(ggg4.gam2, ggg.gam2)
         np.testing.assert_allclose(ggg4.gam3, ggg.gam3)
 
+        ggg4b = treecorr.GGGCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(ggg4b.weight, ggg.weight)
+        np.testing.assert_allclose(ggg4b.meand1, ggg.meand1)
+        np.testing.assert_allclose(ggg4b.meand2, ggg.meand2)
+        np.testing.assert_allclose(ggg4b.meand3, ggg.meand3)
+        np.testing.assert_allclose(ggg4b.meanlogd1, ggg.meanlogd1)
+        np.testing.assert_allclose(ggg4b.meanlogd2, ggg.meanlogd2)
+        np.testing.assert_allclose(ggg4b.meanlogd3, ggg.meanlogd3)
+        np.testing.assert_allclose(ggg4b.gam0, ggg.gam0)
+        np.testing.assert_allclose(ggg4b.gam1, ggg.gam1)
+        np.testing.assert_allclose(ggg4b.gam2, ggg.gam2)
+        np.testing.assert_allclose(ggg4b.gam3, ggg.gam3)
+
     # Split into patches to test the list-based version of the code.
     catp = treecorr.Catalog(x=x, y=y, w=w, g1=g1, g2=g2, npatch=3, rng=rng)
 
@@ -3724,9 +3791,9 @@ def test_ggg_logsas():
     except ImportError:
         pass
     else:
-        out_file_name1 = os.path.join('output','ggg_out1_logsas.fits')
-        ggg.write(out_file_name1)
-        data = fitsio.read(out_file_name1)
+        out_file_name = os.path.join('output','ggg_out_logsas.fits')
+        ggg.write(out_file_name)
+        data = fitsio.read(out_file_name)
         np.testing.assert_allclose(data['d2_nom'], np.exp(ggg.logd2).flatten())
         np.testing.assert_allclose(data['d3_nom'], np.exp(ggg.logd3).flatten())
         np.testing.assert_allclose(data['phi_nom'], ggg.phi.flatten())
@@ -3755,10 +3822,7 @@ def test_ggg_logsas():
         # Check the read function
         # Note: These don't need the flatten.
         # The read function should reshape them to the right shape.
-        ggg2 = treecorr.GGGCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins,
-                                       min_phi=min_phi, max_phi=max_phi, nphi_bins=nphi_bins,
-                                       sep_units='arcmin', phi_units='degrees', bin_type='LogSAS')
-        ggg2.read(out_file_name1)
+        ggg2 = treecorr.GGGCorrelation.from_file(out_file_name)
         np.testing.assert_allclose(ggg2.logd2, ggg.logd2)
         np.testing.assert_allclose(ggg2.logd3, ggg.logd3)
         np.testing.assert_allclose(ggg2.phi, ggg.phi)
@@ -4334,6 +4398,20 @@ def test_direct_logmultipole_auto():
     np.testing.assert_allclose(ggg3.meanlogd2, ggg.meanlogd2)
     np.testing.assert_allclose(ggg3.meanlogd3, ggg.meanlogd3)
 
+    ggg3b = treecorr.GGGCorrelation.from_file(ascii_name)
+    np.testing.assert_allclose(ggg3b.ntri, ggg.ntri)
+    np.testing.assert_allclose(ggg3b.weight, ggg.weight)
+    np.testing.assert_allclose(ggg3b.gam0, ggg.gam0)
+    np.testing.assert_allclose(ggg3b.gam1, ggg.gam1)
+    np.testing.assert_allclose(ggg3b.gam2, ggg.gam2)
+    np.testing.assert_allclose(ggg3b.gam3, ggg.gam3)
+    np.testing.assert_allclose(ggg3b.meand1, ggg.meand1)
+    np.testing.assert_allclose(ggg3b.meand2, ggg.meand2)
+    np.testing.assert_allclose(ggg3b.meand3, ggg.meand3)
+    np.testing.assert_allclose(ggg3b.meanlogd1, ggg.meanlogd1)
+    np.testing.assert_allclose(ggg3b.meanlogd2, ggg.meanlogd2)
+    np.testing.assert_allclose(ggg3b.meanlogd3, ggg.meanlogd3)
+
     try:
         import fitsio
     except ImportError:
@@ -4357,6 +4435,20 @@ def test_direct_logmultipole_auto():
         np.testing.assert_allclose(ggg4.meanlogd2, ggg.meanlogd2)
         np.testing.assert_allclose(ggg4.meanlogd3, ggg.meanlogd3)
 
+        ggg4b = treecorr.GGGCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(ggg4b.ntri, ggg.ntri)
+        np.testing.assert_allclose(ggg4b.weight, ggg.weight)
+        np.testing.assert_allclose(ggg3b.gam0, ggg.gam0)
+        np.testing.assert_allclose(ggg3b.gam1, ggg.gam1)
+        np.testing.assert_allclose(ggg3b.gam2, ggg.gam2)
+        np.testing.assert_allclose(ggg3b.gam3, ggg.gam3)
+        np.testing.assert_allclose(ggg4b.meand1, ggg.meand1)
+        np.testing.assert_allclose(ggg4b.meand2, ggg.meand2)
+        np.testing.assert_allclose(ggg4b.meand3, ggg.meand3)
+        np.testing.assert_allclose(ggg4b.meanlogd1, ggg.meanlogd1)
+        np.testing.assert_allclose(ggg4b.meanlogd2, ggg.meanlogd2)
+        np.testing.assert_allclose(ggg4b.meanlogd3, ggg.meanlogd3)
+
 @timer
 def test_direct_logmultipole_spherical():
     # Repeat in spherical coords
@@ -4867,7 +4959,6 @@ def test_map3_logmultipole():
     np.testing.assert_allclose(map3, true_map3[mask], rtol=0.1)
 
     # The default is to use max_n phi bins, which should also work fine here.
-    gggm.read(os.path.join('data',out_name))
     ggg = gggm.toSAS()
     map3 = ggg.calculateMap3(R=R)[0]
     np.testing.assert_allclose(map3, true_map3[mask], rtol=0.1)
diff --git a/tests/test_kg.py b/tests/test_kg.py
index c6d536a0..1867f094 100644
--- a/tests/test_kg.py
+++ b/tests/test_kg.py
@@ -147,7 +147,7 @@ def test_direct():
 
     ascii_name = 'output/kg_ascii.txt'
     kg.write(ascii_name, precision=16)
-    kg3 = treecorr.KGCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    kg3 = treecorr.KGCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     kg3.read(ascii_name)
     np.testing.assert_allclose(kg3.npairs, kg.npairs)
     np.testing.assert_allclose(kg3.weight, kg.weight)
@@ -156,6 +156,20 @@ def test_direct():
     np.testing.assert_allclose(kg3.xi, kg.xi)
     np.testing.assert_allclose(kg3.xi_im, kg.xi_im)
 
+    # Check that the repr is minimal
+    assert repr(kg3) == f'KGCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # New in version 5.0 is a simpler API for reading
+    with CaptureLog() as cl:
+        kg3b = treecorr.KGCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(kg3b.npairs, kg.npairs)
+    np.testing.assert_allclose(kg3b.weight, kg.weight)
+    np.testing.assert_allclose(kg3b.meanr, kg.meanr)
+    np.testing.assert_allclose(kg3b.meanlogr, kg.meanlogr)
+    np.testing.assert_allclose(kg3b.xi, kg.xi)
+    np.testing.assert_allclose(kg3b.xi_im, kg.xi_im)
+
     try:
         import fitsio
     except ImportError:
@@ -172,6 +186,14 @@ def test_direct():
         np.testing.assert_allclose(kg4.xi, kg.xi)
         np.testing.assert_allclose(kg4.xi_im, kg.xi_im)
 
+        kg4b = treecorr.KGCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(kg4b.npairs, kg.npairs)
+        np.testing.assert_allclose(kg4b.weight, kg.weight)
+        np.testing.assert_allclose(kg4b.meanr, kg.meanr)
+        np.testing.assert_allclose(kg4b.meanlogr, kg.meanlogr)
+        np.testing.assert_allclose(kg4b.xi, kg.xi)
+        np.testing.assert_allclose(kg4b.xi_im, kg.xi_im)
+
     with assert_raises(TypeError):
         kg2 += config
     kg4 = treecorr.KGCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -446,9 +468,9 @@ def test_kg():
     except ImportError:
         pass
     else:
-        out_file_name1 = os.path.join('output','kg_out1.fits')
-        kg.write(out_file_name1)
-        data = fitsio.read(out_file_name1)
+        out_file_name = os.path.join('output','kg_out.fits')
+        kg.write(out_file_name)
+        data = fitsio.read(out_file_name)
         np.testing.assert_almost_equal(data['r_nom'], np.exp(kg.logr))
         np.testing.assert_almost_equal(data['meanr'], kg.meanr)
         np.testing.assert_almost_equal(data['meanlogr'], kg.meanlogr)
@@ -459,8 +481,7 @@ def test_kg():
         np.testing.assert_almost_equal(data['npairs'], kg.npairs)
 
         # Check the read function
-        kg2 = treecorr.KGCorrelation(bin_size=0.1, min_sep=1., max_sep=20., sep_units='arcmin')
-        kg2.read(out_file_name1)
+        kg2 = treecorr.KGCorrelation.from_file(out_file_name)
         np.testing.assert_almost_equal(kg2.logr, kg.logr)
         np.testing.assert_almost_equal(kg2.meanr, kg.meanr)
         np.testing.assert_almost_equal(kg2.meanlogr, kg.meanlogr)
diff --git a/tests/test_kk.py b/tests/test_kk.py
index 9c9c2e29..297f2aed 100644
--- a/tests/test_kk.py
+++ b/tests/test_kk.py
@@ -125,7 +125,7 @@ def test_direct():
 
     ascii_name = 'output/kk_ascii.txt'
     kk.write(ascii_name, precision=16)
-    kk3 = treecorr.KKCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    kk3 = treecorr.KKCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     kk3.read(ascii_name)
     np.testing.assert_allclose(kk3.npairs, kk.npairs)
     np.testing.assert_allclose(kk3.weight, kk.weight)
@@ -133,6 +133,19 @@ def test_direct():
     np.testing.assert_allclose(kk3.meanlogr, kk.meanlogr)
     np.testing.assert_allclose(kk3.xi, kk.xi)
 
+    # Check that the repr is minimal
+    assert repr(kk3) == f'KKCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # New in version 5.0 is a simpler API for reading
+    with CaptureLog() as cl:
+        kk3b = treecorr.KKCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(kk3b.npairs, kk.npairs)
+    np.testing.assert_allclose(kk3b.weight, kk.weight)
+    np.testing.assert_allclose(kk3b.meanr, kk.meanr)
+    np.testing.assert_allclose(kk3b.meanlogr, kk.meanlogr)
+    np.testing.assert_allclose(kk3b.xi, kk.xi)
+
     try:
         import fitsio
     except ImportError:
@@ -148,6 +161,13 @@ def test_direct():
         np.testing.assert_allclose(kk4.meanlogr, kk.meanlogr)
         np.testing.assert_allclose(kk4.xi, kk.xi)
 
+        kk4b = treecorr.KKCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(kk4b.npairs, kk.npairs)
+        np.testing.assert_allclose(kk4b.weight, kk.weight)
+        np.testing.assert_allclose(kk4b.meanr, kk.meanr)
+        np.testing.assert_allclose(kk4b.meanlogr, kk.meanlogr)
+        np.testing.assert_allclose(kk4b.xi, kk.xi)
+
     with assert_raises(TypeError):
         kk2 += config
     kk4 = treecorr.KKCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -453,8 +473,7 @@ def test_kk():
         np.testing.assert_almost_equal(data['npairs'], kk.npairs)
 
         # Check the read function
-        kk2 = treecorr.KKCorrelation(bin_size=0.1, min_sep=1., max_sep=20., sep_units='arcmin')
-        kk2.read(out_file_name)
+        kk2 = treecorr.KKCorrelation.from_file(out_file_name)
         np.testing.assert_almost_equal(kk2.logr, kk.logr)
         np.testing.assert_almost_equal(kk2.meanr, kk.meanr)
         np.testing.assert_almost_equal(kk2.meanlogr, kk.meanlogr)
diff --git a/tests/test_kkk.py b/tests/test_kkk.py
index 8d165a69..24b1aba9 100644
--- a/tests/test_kkk.py
+++ b/tests/test_kkk.py
@@ -17,7 +17,7 @@
 import coord
 import time
 
-from test_helper import do_pickle, assert_raises, timer, is_ccw, is_ccw_3d
+from test_helper import do_pickle, assert_raises, timer, is_ccw, is_ccw_3d, CaptureLog
 
 @timer
 def test_direct_logruv():
@@ -199,6 +199,22 @@ def test_direct_logruv():
     np.testing.assert_allclose(kkk3.meanv, kkk.meanv)
     np.testing.assert_allclose(kkk3.zeta, kkk.zeta)
 
+    # New in version 5.0 is a simpler API for reading
+    with CaptureLog() as cl:
+        kkk3b = treecorr.KKKCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(kkk3b.ntri, kkk.ntri)
+    np.testing.assert_allclose(kkk3b.weight, kkk.weight)
+    np.testing.assert_allclose(kkk3b.meand1, kkk.meand1)
+    np.testing.assert_allclose(kkk3b.meand2, kkk.meand2)
+    np.testing.assert_allclose(kkk3b.meand3, kkk.meand3)
+    np.testing.assert_allclose(kkk3b.meanlogd1, kkk.meanlogd1)
+    np.testing.assert_allclose(kkk3b.meanlogd2, kkk.meanlogd2)
+    np.testing.assert_allclose(kkk3b.meanlogd3, kkk.meanlogd3)
+    np.testing.assert_allclose(kkk3b.meanu, kkk.meanu)
+    np.testing.assert_allclose(kkk3b.meanv, kkk.meanv)
+    np.testing.assert_allclose(kkk3b.zeta, kkk.zeta)
+
     try:
         import fitsio
     except ImportError:
@@ -221,6 +237,19 @@ def test_direct_logruv():
         np.testing.assert_allclose(kkk4.meanv, kkk.meanv)
         np.testing.assert_allclose(kkk4.zeta, kkk.zeta)
 
+        kkk4b = treecorr.KKKCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(kkk4b.ntri, kkk.ntri)
+        np.testing.assert_allclose(kkk4b.weight, kkk.weight)
+        np.testing.assert_allclose(kkk4b.meand1, kkk.meand1)
+        np.testing.assert_allclose(kkk4b.meand2, kkk.meand2)
+        np.testing.assert_allclose(kkk4b.meand3, kkk.meand3)
+        np.testing.assert_allclose(kkk4b.meanlogd1, kkk.meanlogd1)
+        np.testing.assert_allclose(kkk4b.meanlogd2, kkk.meanlogd2)
+        np.testing.assert_allclose(kkk4b.meanlogd3, kkk.meanlogd3)
+        np.testing.assert_allclose(kkk4b.meanu, kkk.meanu)
+        np.testing.assert_allclose(kkk4b.meanv, kkk.meanv)
+        np.testing.assert_allclose(kkk4b.zeta, kkk.zeta)
+
     with assert_raises(TypeError):
         kkk2 += config
     kkk5 = treecorr.KKKCorrelation(min_sep=min_sep/2, bin_size=bin_size, nbins=nrbins,
@@ -1211,11 +1240,7 @@ def test_kkk_logruv():
         # Check the read function
         # Note: These don't need the flatten.
         # The read function should reshape them to the right shape.
-        kkk2 = treecorr.KKKCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins,
-                                       min_u=min_u, max_u=max_u, min_v=min_v, max_v=max_v,
-                                       nubins=nubins, nvbins=nvbins,
-                                       sep_units='arcmin', verbose=1, bin_type='LogRUV')
-        kkk2.read(out_file_name)
+        kkk2 = treecorr.KKKCorrelation.from_file(out_file_name)
         np.testing.assert_allclose(kkk2.logr, kkk.logr, rtol=1.e-4)
         np.testing.assert_allclose(kkk2.u, kkk.u, rtol=1.e-4)
         np.testing.assert_allclose(kkk2.v, kkk.v, rtol=1.e-4)
@@ -1368,6 +1393,22 @@ def test_direct_logsas():
     np.testing.assert_allclose(kkk3.meanphi, kkk.meanphi)
     np.testing.assert_allclose(kkk3.zeta, kkk.zeta)
 
+    # Check that the repr is minimal
+    assert repr(kkk3) == f"KKKCorrelation(min_sep={min_sep}, bin_size={bin_size}, nbins={nbins}, nphi_bins={nphi_bins})"
+
+    # New in version 5.0 is a simpler API for reading
+    kkk3b = treecorr.KKKCorrelation.from_file(ascii_name)
+    np.testing.assert_allclose(kkk3b.ntri, kkk.ntri)
+    np.testing.assert_allclose(kkk3b.weight, kkk.weight)
+    np.testing.assert_allclose(kkk3b.meand1, kkk.meand1)
+    np.testing.assert_allclose(kkk3b.meand2, kkk.meand2)
+    np.testing.assert_allclose(kkk3b.meand3, kkk.meand3)
+    np.testing.assert_allclose(kkk3b.meanlogd1, kkk.meanlogd1)
+    np.testing.assert_allclose(kkk3b.meanlogd2, kkk.meanlogd2)
+    np.testing.assert_allclose(kkk3b.meanlogd3, kkk.meanlogd3)
+    np.testing.assert_allclose(kkk3b.meanphi, kkk.meanphi)
+    np.testing.assert_allclose(kkk3b.zeta, kkk.zeta)
+
     try:
         import fitsio
     except ImportError:
@@ -1389,6 +1430,18 @@ def test_direct_logsas():
         np.testing.assert_allclose(kkk4.meanphi, kkk.meanphi)
         np.testing.assert_allclose(kkk4.zeta, kkk.zeta)
 
+        kkk4b = treecorr.KKKCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(kkk4b.ntri, kkk.ntri)
+        np.testing.assert_allclose(kkk4b.weight, kkk.weight)
+        np.testing.assert_allclose(kkk4b.meand1, kkk.meand1)
+        np.testing.assert_allclose(kkk4b.meand2, kkk.meand2)
+        np.testing.assert_allclose(kkk4b.meand3, kkk.meand3)
+        np.testing.assert_allclose(kkk4b.meanlogd1, kkk.meanlogd1)
+        np.testing.assert_allclose(kkk4b.meanlogd2, kkk.meanlogd2)
+        np.testing.assert_allclose(kkk4b.meanlogd3, kkk.meanlogd3)
+        np.testing.assert_allclose(kkk4b.meanphi, kkk.meanphi)
+        np.testing.assert_allclose(kkk4b.zeta, kkk.zeta)
+
     # The above all used the old triangle algorithm.  Check the new default of calculating
     # LogSAS via a temporary object with LogMultipole.
     kkk2 = treecorr.KKKCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins,
@@ -2228,10 +2281,7 @@ def test_kkk_logsas():
         # Check the read function
         # Note: These don't need the flatten.
         # The read function should reshape them to the right shape.
-        kkk2 = treecorr.KKKCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins,
-                                       min_phi=min_phi, max_phi=max_phi, nphi_bins=nphi_bins,
-                                       sep_units='arcmin', phi_units='deg', bin_type='LogSAS')
-        kkk2.read(out_file_name)
+        kkk2 = treecorr.KKKCorrelation.from_file(out_file_name)
         np.testing.assert_allclose(kkk2.logd2, kkk.logd2)
         np.testing.assert_allclose(kkk2.logd3, kkk.logd3)
         np.testing.assert_allclose(kkk2.phi, kkk.phi)
@@ -2378,6 +2428,17 @@ def test_direct_logmultipole_auto():
     np.testing.assert_allclose(kkk3.meanlogd2, kkk.meanlogd2)
     np.testing.assert_allclose(kkk3.meanlogd3, kkk.meanlogd3)
 
+    kkk3b = treecorr.KKKCorrelation.from_file(ascii_name)
+    np.testing.assert_allclose(kkk3b.ntri, kkk.ntri)
+    np.testing.assert_allclose(kkk3b.weight, kkk.weight)
+    np.testing.assert_allclose(kkk3b.zeta, kkk.zeta)
+    np.testing.assert_allclose(kkk3b.meand1, kkk.meand1)
+    np.testing.assert_allclose(kkk3b.meand2, kkk.meand2)
+    np.testing.assert_allclose(kkk3b.meand3, kkk.meand3)
+    np.testing.assert_allclose(kkk3b.meanlogd1, kkk.meanlogd1)
+    np.testing.assert_allclose(kkk3b.meanlogd2, kkk.meanlogd2)
+    np.testing.assert_allclose(kkk3b.meanlogd3, kkk.meanlogd3)
+
     try:
         import fitsio
     except ImportError:
@@ -2398,6 +2459,17 @@ def test_direct_logmultipole_auto():
         np.testing.assert_allclose(kkk4.meanlogd2, kkk.meanlogd2)
         np.testing.assert_allclose(kkk4.meanlogd3, kkk.meanlogd3)
 
+        kkk4b = treecorr.KKKCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(kkk4b.ntri, kkk.ntri)
+        np.testing.assert_allclose(kkk4b.weight, kkk.weight)
+        np.testing.assert_allclose(kkk3b.zeta, kkk.zeta)
+        np.testing.assert_allclose(kkk4b.meand1, kkk.meand1)
+        np.testing.assert_allclose(kkk4b.meand2, kkk.meand2)
+        np.testing.assert_allclose(kkk4b.meand3, kkk.meand3)
+        np.testing.assert_allclose(kkk4b.meanlogd1, kkk.meanlogd1)
+        np.testing.assert_allclose(kkk4b.meanlogd2, kkk.meanlogd2)
+        np.testing.assert_allclose(kkk4b.meanlogd3, kkk.meanlogd3)
+
 
 @timer
 def test_direct_logmultipole_spherical():
diff --git a/tests/test_kq.py b/tests/test_kq.py
index c2fb4a97..e27c04de 100644
--- a/tests/test_kq.py
+++ b/tests/test_kq.py
@@ -148,7 +148,7 @@ def test_direct():
 
     ascii_name = 'output/kq_ascii.txt'
     kq.write(ascii_name, precision=16)
-    kq3 = treecorr.KQCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    kq3 = treecorr.KQCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     kq3.read(ascii_name)
     np.testing.assert_allclose(kq3.npairs, kq.npairs)
     np.testing.assert_allclose(kq3.weight, kq.weight)
@@ -157,6 +157,20 @@ def test_direct():
     np.testing.assert_allclose(kq3.xi, kq.xi)
     np.testing.assert_allclose(kq3.xi_im, kq.xi_im)
 
+    # Check that the repr is minimal
+    assert repr(kq3) == f'KQCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # New in version 5.0 is a simpler API for reading
+    with CaptureLog() as cl:
+        kq3b = treecorr.KQCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(kq3b.npairs, kq.npairs)
+    np.testing.assert_allclose(kq3b.weight, kq.weight)
+    np.testing.assert_allclose(kq3b.meanr, kq.meanr)
+    np.testing.assert_allclose(kq3b.meanlogr, kq.meanlogr)
+    np.testing.assert_allclose(kq3b.xi, kq.xi)
+    np.testing.assert_allclose(kq3b.xi_im, kq.xi_im)
+
     try:
         import fitsio
     except ImportError:
@@ -173,6 +187,14 @@ def test_direct():
         np.testing.assert_allclose(kq4.xi, kq.xi)
         np.testing.assert_allclose(kq4.xi_im, kq.xi_im)
 
+        kq4b = treecorr.KQCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(kq4b.npairs, kq.npairs)
+        np.testing.assert_allclose(kq4b.weight, kq.weight)
+        np.testing.assert_allclose(kq4b.meanr, kq.meanr)
+        np.testing.assert_allclose(kq4b.meanlogr, kq.meanlogr)
+        np.testing.assert_allclose(kq4b.xi, kq.xi)
+        np.testing.assert_allclose(kq4b.xi_im, kq.xi_im)
+
     with assert_raises(TypeError):
         kq2 += config
     kq4 = treecorr.KQCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -450,9 +472,9 @@ def test_kq():
     except ImportError:
         pass
     else:
-        out_file_name1 = os.path.join('output','kg_out1.fits')
-        kq.write(out_file_name1)
-        data = fitsio.read(out_file_name1)
+        out_file_name = os.path.join('output','kg_out.fits')
+        kq.write(out_file_name)
+        data = fitsio.read(out_file_name)
         np.testing.assert_almost_equal(data['r_nom'], np.exp(kq.logr))
         np.testing.assert_almost_equal(data['meanr'], kq.meanr)
         np.testing.assert_almost_equal(data['meanlogr'], kq.meanlogr)
@@ -463,8 +485,7 @@ def test_kq():
         np.testing.assert_almost_equal(data['npairs'], kq.npairs)
 
         # Check the read function
-        kq2 = treecorr.KQCorrelation(bin_size=0.1, min_sep=1., max_sep=20., sep_units='arcmin')
-        kq2.read(out_file_name1)
+        kq2 = treecorr.KQCorrelation.from_file(out_file_name)
         np.testing.assert_almost_equal(kq2.logr, kq.logr)
         np.testing.assert_almost_equal(kq2.meanr, kq.meanr)
         np.testing.assert_almost_equal(kq2.meanlogr, kq.meanlogr)
@@ -708,8 +729,7 @@ def make_spin4_field(rng):
     else:
         file_name = os.path.join('output','test_write_results_kq.fits')
         kq2.write(file_name, write_patch_results=True)
-        kq5 = treecorr.KQCorrelation(corr_params)
-        kq5.read(file_name)
+        kq5 = treecorr.KQCorrelation.from_file(file_name)
         cov5 = kq5.estimate_cov('jackknife')
         np.testing.assert_allclose(cov5, cov2)
 
diff --git a/tests/test_kt.py b/tests/test_kt.py
index e59d3a39..fd370475 100644
--- a/tests/test_kt.py
+++ b/tests/test_kt.py
@@ -148,7 +148,7 @@ def test_direct():
 
     ascii_name = 'output/kt_ascii.txt'
     kt.write(ascii_name, precision=16)
-    kt3 = treecorr.KTCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    kt3 = treecorr.KTCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     kt3.read(ascii_name)
     np.testing.assert_allclose(kt3.npairs, kt.npairs)
     np.testing.assert_allclose(kt3.weight, kt.weight)
@@ -157,6 +157,20 @@ def test_direct():
     np.testing.assert_allclose(kt3.xi, kt.xi)
     np.testing.assert_allclose(kt3.xi_im, kt.xi_im)
 
+    # Check that the repr is minimal
+    assert repr(kt3) == f'KTCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # Simpler API using from_file:
+    with CaptureLog() as cl:
+        kt3b = treecorr.KTCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(kt3b.npairs, kt.npairs)
+    np.testing.assert_allclose(kt3b.weight, kt.weight)
+    np.testing.assert_allclose(kt3b.meanr, kt.meanr)
+    np.testing.assert_allclose(kt3b.meanlogr, kt.meanlogr)
+    np.testing.assert_allclose(kt3b.xi, kt.xi)
+    np.testing.assert_allclose(kt3b.xi_im, kt.xi_im)
+
     try:
         import fitsio
     except ImportError:
@@ -173,6 +187,14 @@ def test_direct():
         np.testing.assert_allclose(kt4.xi, kt.xi)
         np.testing.assert_allclose(kt4.xi_im, kt.xi_im)
 
+        kt4b = treecorr.KTCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(kt4b.npairs, kt.npairs)
+        np.testing.assert_allclose(kt4b.weight, kt.weight)
+        np.testing.assert_allclose(kt4b.meanr, kt.meanr)
+        np.testing.assert_allclose(kt4b.meanlogr, kt.meanlogr)
+        np.testing.assert_allclose(kt4b.xi, kt.xi)
+        np.testing.assert_allclose(kt4b.xi_im, kt.xi_im)
+
     with assert_raises(TypeError):
         kt2 += config
     kt4 = treecorr.KTCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -449,9 +471,9 @@ def test_kt():
     except ImportError:
         pass
     else:
-        out_file_name1 = os.path.join('output','kg_out1.fits')
-        kt.write(out_file_name1)
-        data = fitsio.read(out_file_name1)
+        out_file_name = os.path.join('output','kg_out1.fits')
+        kt.write(out_file_name)
+        data = fitsio.read(out_file_name)
         np.testing.assert_almost_equal(data['r_nom'], np.exp(kt.logr))
         np.testing.assert_almost_equal(data['meanr'], kt.meanr)
         np.testing.assert_almost_equal(data['meanlogr'], kt.meanlogr)
@@ -462,8 +484,7 @@ def test_kt():
         np.testing.assert_almost_equal(data['npairs'], kt.npairs)
 
         # Check the read function
-        kt2 = treecorr.KTCorrelation(bin_size=0.1, min_sep=1., max_sep=20., sep_units='arcmin')
-        kt2.read(out_file_name1)
+        kt2 = treecorr.KTCorrelation.from_file(out_file_name)
         np.testing.assert_almost_equal(kt2.logr, kt.logr)
         np.testing.assert_almost_equal(kt2.meanr, kt.meanr)
         np.testing.assert_almost_equal(kt2.meanlogr, kt.meanlogr)
@@ -707,8 +728,7 @@ def make_spin3_field(rng):
     else:
         file_name = os.path.join('output','test_write_results_kt.fits')
         kt2.write(file_name, write_patch_results=True)
-        kt5 = treecorr.KTCorrelation(corr_params)
-        kt5.read(file_name)
+        kt5 = treecorr.KTCorrelation.from_file(file_name)
         cov5 = kt5.estimate_cov('jackknife')
         np.testing.assert_allclose(cov5, cov2)
 
diff --git a/tests/test_kv.py b/tests/test_kv.py
index 0a79061a..e52b7795 100644
--- a/tests/test_kv.py
+++ b/tests/test_kv.py
@@ -148,7 +148,7 @@ def test_direct():
 
     ascii_name = 'output/kv_ascii.txt'
     kv.write(ascii_name, precision=16)
-    kv3 = treecorr.KVCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    kv3 = treecorr.KVCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     kv3.read(ascii_name)
     np.testing.assert_allclose(kv3.npairs, kv.npairs)
     np.testing.assert_allclose(kv3.weight, kv.weight)
@@ -157,6 +157,20 @@ def test_direct():
     np.testing.assert_allclose(kv3.xi, kv.xi)
     np.testing.assert_allclose(kv3.xi_im, kv.xi_im)
 
+    # Check that the repr is minimal
+    assert repr(kv3) == f'KVCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # Simpler API using from_file:
+    with CaptureLog() as cl:
+        kv3b = treecorr.KVCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(kv3b.npairs, kv.npairs)
+    np.testing.assert_allclose(kv3b.weight, kv.weight)
+    np.testing.assert_allclose(kv3b.meanr, kv.meanr)
+    np.testing.assert_allclose(kv3b.meanlogr, kv.meanlogr)
+    np.testing.assert_allclose(kv3b.xi, kv.xi)
+    np.testing.assert_allclose(kv3b.xi_im, kv.xi_im)
+
     try:
         import fitsio
     except ImportError:
@@ -173,6 +187,14 @@ def test_direct():
         np.testing.assert_allclose(kv4.xi, kv.xi)
         np.testing.assert_allclose(kv4.xi_im, kv.xi_im)
 
+        kv4b = treecorr.KVCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(kv4b.npairs, kv.npairs)
+        np.testing.assert_allclose(kv4b.weight, kv.weight)
+        np.testing.assert_allclose(kv4b.meanr, kv.meanr)
+        np.testing.assert_allclose(kv4b.meanlogr, kv.meanlogr)
+        np.testing.assert_allclose(kv4b.xi, kv.xi)
+        np.testing.assert_allclose(kv4b.xi_im, kv.xi_im)
+
     with assert_raises(TypeError):
         kv2 += config
     kv4 = treecorr.KVCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -460,8 +482,7 @@ def test_kv():
         np.testing.assert_almost_equal(data['npairs'], kv.npairs)
 
         # Check the read function
-        kv2 = treecorr.KVCorrelation(bin_size=0.1, min_sep=1., max_sep=20., sep_units='arcmin')
-        kv2.read(out_file_name1)
+        kv2 = treecorr.KVCorrelation.from_file(out_file_name1)
         np.testing.assert_almost_equal(kv2.logr, kv.logr)
         np.testing.assert_almost_equal(kv2.meanr, kv.meanr)
         np.testing.assert_almost_equal(kv2.meanlogr, kv.meanlogr)
@@ -717,8 +738,7 @@ def make_velocity_field(rng):
     else:
         file_name = os.path.join('output','test_write_results_kv.fits')
         kv2.write(file_name, write_patch_results=True)
-        kv5 = treecorr.KVCorrelation(corr_params)
-        kv5.read(file_name)
+        kv5 = treecorr.KVCorrelation.from_file(file_name)
         cov5 = kv5.estimate_cov('jackknife')
         np.testing.assert_allclose(cov5, cov2)
 
diff --git a/tests/test_ng.py b/tests/test_ng.py
index 02273a42..92fd91ee 100644
--- a/tests/test_ng.py
+++ b/tests/test_ng.py
@@ -157,7 +157,7 @@ def test_direct():
 
     ascii_name = 'output/ng_ascii.txt'
     ng.write(ascii_name, precision=16)
-    ng3 = treecorr.NGCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    ng3 = treecorr.NGCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     ng3.read(ascii_name)
     np.testing.assert_allclose(ng3.npairs, ng.npairs)
     np.testing.assert_allclose(ng3.weight, ng.weight)
@@ -166,6 +166,20 @@ def test_direct():
     np.testing.assert_allclose(ng3.xi, ng.xi)
     np.testing.assert_allclose(ng3.xi_im, ng.xi_im)
 
+    # Check that the repr is minimal
+    assert repr(ng3) == f'NGCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # Simpler API using from_file:
+    with CaptureLog() as cl:
+        ng3b = treecorr.NGCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(ng3b.npairs, ng.npairs)
+    np.testing.assert_allclose(ng3b.weight, ng.weight)
+    np.testing.assert_allclose(ng3b.meanr, ng.meanr)
+    np.testing.assert_allclose(ng3b.meanlogr, ng.meanlogr)
+    np.testing.assert_allclose(ng3b.xi, ng.xi)
+    np.testing.assert_allclose(ng3b.xi_im, ng.xi_im)
+
     try:
         import fitsio
     except ImportError:
@@ -182,6 +196,14 @@ def test_direct():
         np.testing.assert_allclose(ng4.xi, ng.xi)
         np.testing.assert_allclose(ng4.xi_im, ng.xi_im)
 
+        ng4b = treecorr.NGCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(ng4b.npairs, ng.npairs)
+        np.testing.assert_allclose(ng4b.weight, ng.weight)
+        np.testing.assert_allclose(ng4b.meanr, ng.meanr)
+        np.testing.assert_allclose(ng4b.meanlogr, ng.meanlogr)
+        np.testing.assert_allclose(ng4b.xi, ng.xi)
+        np.testing.assert_allclose(ng4b.xi_im, ng.xi_im)
+
     with assert_raises(TypeError):
         ng2 += config
     ng4 = treecorr.NGCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -666,8 +688,7 @@ def test_ng():
         np.testing.assert_almost_equal(data['npairs'], ng.npairs)
 
         # Check the read function
-        ng2 = treecorr.NGCorrelation(bin_size=0.1, min_sep=1., max_sep=20., sep_units='arcmin')
-        ng2.read(out_file_name2)
+        ng2 = treecorr.NGCorrelation.from_file(out_file_name2)
         np.testing.assert_almost_equal(ng2.logr, ng.logr)
         np.testing.assert_almost_equal(ng2.meanr, ng.meanr)
         np.testing.assert_almost_equal(ng2.meanlogr, ng.meanlogr)
diff --git a/tests/test_nk.py b/tests/test_nk.py
index bdd80db1..4d893d3d 100644
--- a/tests/test_nk.py
+++ b/tests/test_nk.py
@@ -138,7 +138,7 @@ def test_direct():
 
     ascii_name = 'output/nk_ascii.txt'
     nk.write(ascii_name, precision=16)
-    nk3 = treecorr.NKCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    nk3 = treecorr.NKCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     nk3.read(ascii_name)
     np.testing.assert_allclose(nk3.npairs, nk.npairs)
     np.testing.assert_allclose(nk3.weight, nk.weight)
@@ -146,6 +146,19 @@ def test_direct():
     np.testing.assert_allclose(nk3.meanlogr, nk.meanlogr)
     np.testing.assert_allclose(nk3.xi, nk.xi)
 
+    # Check that the repr is minimal
+    assert repr(nk3) == f'NKCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # Simpler API using from_file:
+    with CaptureLog() as cl:
+        nk3b = treecorr.NKCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(nk3b.npairs, nk.npairs)
+    np.testing.assert_allclose(nk3b.weight, nk.weight)
+    np.testing.assert_allclose(nk3b.meanr, nk.meanr)
+    np.testing.assert_allclose(nk3b.meanlogr, nk.meanlogr)
+    np.testing.assert_allclose(nk3b.xi, nk.xi)
+
     with assert_raises(TypeError):
         nk2 += config
     nk4 = treecorr.NKCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -173,6 +186,13 @@ def test_direct():
         np.testing.assert_allclose(nk4.meanlogr, nk.meanlogr)
         np.testing.assert_allclose(nk4.xi, nk.xi)
 
+        nk4b = treecorr.NKCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(nk4b.npairs, nk.npairs)
+        np.testing.assert_allclose(nk4b.weight, nk.weight)
+        np.testing.assert_allclose(nk4b.meanr, nk.meanr)
+        np.testing.assert_allclose(nk4b.meanlogr, nk.meanlogr)
+        np.testing.assert_allclose(nk4b.xi, nk.xi)
+
     with assert_raises(ValueError):
         nk.process(cat1, cat2, patch_method='nonlocal')
 
@@ -451,8 +471,7 @@ def test_nk():
         np.testing.assert_almost_equal(data['npairs'], nk.npairs)
 
         # Check the read function
-        nk2 = treecorr.NKCorrelation(bin_size=0.1, min_sep=1., max_sep=20., sep_units='arcmin')
-        nk2.read(out_file_name2)
+        nk2 = treecorr.NKCorrelation.from_file(out_file_name2)
         np.testing.assert_almost_equal(nk2.logr, nk.logr)
         np.testing.assert_almost_equal(nk2.meanr, nk.meanr)
         np.testing.assert_almost_equal(nk2.meanlogr, nk.meanlogr)
diff --git a/tests/test_nn.py b/tests/test_nn.py
index 88b17d56..184e6ffb 100644
--- a/tests/test_nn.py
+++ b/tests/test_nn.py
@@ -1242,7 +1242,8 @@ def test_nn():
 
     # Check the read function (not at very high accuracy for the ASCII I/O)
     dd.calculateXi(rr=rr, dr=dr)  # reset this to the better calculation
-    dd2 = treecorr.NNCorrelation(bin_size=0.1, min_sep=1., max_sep=25., sep_units='arcmin')
+    dd2 = treecorr.NNCorrelation(bin_size=0.1, min_sep=1., max_sep=25., sep_units='arcmin',
+                                 bin_type='Log')
     dd2.read(out_file_name)
     np.testing.assert_allclose(dd2.logr, dd.logr, rtol=1.e-3)
     np.testing.assert_allclose(dd2.meanr, dd.meanr, rtol=1.e-3)
@@ -1256,6 +1257,24 @@ def test_nn():
     assert dd2.sep_units == dd.sep_units
     assert dd2.bin_type == dd.bin_type
 
+    # Check that the repr is minimal
+    assert repr(dd2) == f"NNCorrelation(bin_size=0.1, min_sep=1.0, max_sep=25.0, sep_units='arcmin')"
+    # Simpler API using from_file:
+    with CaptureLog() as cl:
+        dd2b = treecorr.NNCorrelation.from_file(out_file_name, logger=cl.logger)
+    assert out_file_name in cl.output
+    np.testing.assert_allclose(dd2b.logr, dd.logr, rtol=1.e-3)
+    np.testing.assert_allclose(dd2b.meanr, dd.meanr, rtol=1.e-3)
+    np.testing.assert_allclose(dd2b.meanlogr, dd.meanlogr, rtol=1.e-3)
+    np.testing.assert_allclose(dd2b.npairs, dd.npairs, rtol=1.e-3)
+    np.testing.assert_allclose(dd2b.tot, dd.tot, rtol=1.e-3)
+    np.testing.assert_allclose(dd2b.xi, dd.xi, rtol=1.e-3)
+    np.testing.assert_allclose(dd2b.varxi, dd.varxi, rtol=1.e-3)
+    assert dd2b.coords == dd.coords
+    assert dd2b.metric == dd.metric
+    assert dd2b.sep_units == dd.sep_units
+    assert dd2b.bin_type == dd.bin_type
+
     # Check the fits write option
     try:
         import fitsio
@@ -1345,6 +1364,19 @@ def test_nn():
         assert dd2.sep_units == dd.sep_units
         assert dd2.bin_type == dd.bin_type
 
+        dd2b = treecorr.NNCorrelation.from_file(out_file_name1)
+        np.testing.assert_almost_equal(dd2b.logr, dd.logr)
+        np.testing.assert_almost_equal(dd2b.meanr, dd.meanr)
+        np.testing.assert_almost_equal(dd2b.meanlogr, dd.meanlogr)
+        np.testing.assert_almost_equal(dd2b.npairs, dd.npairs)
+        np.testing.assert_almost_equal(dd2b.tot, dd.tot)
+        np.testing.assert_almost_equal(dd2b.xi, dd.xi)
+        np.testing.assert_almost_equal(dd2b.varxi, dd.varxi)
+        assert dd2b.coords == dd.coords
+        assert dd2b.metric == dd.metric
+        assert dd2b.sep_units == dd.sep_units
+        assert dd2b.bin_type == dd.bin_type
+
         dd3 = treecorr.NNCorrelation(bin_size=0.1, min_sep=1., max_sep=25., sep_units='arcmin')
         dd3.read(out_file_name3)
         np.testing.assert_almost_equal(dd3.logr, dd.logr)
@@ -1359,6 +1391,19 @@ def test_nn():
         assert dd3.sep_units == dd.sep_units
         assert dd3.bin_type == dd.bin_type
 
+        dd3b = treecorr.NNCorrelation.from_file(out_file_name3)
+        np.testing.assert_almost_equal(dd3b.logr, dd.logr)
+        np.testing.assert_almost_equal(dd3b.meanr, dd.meanr)
+        np.testing.assert_almost_equal(dd3b.meanlogr, dd.meanlogr)
+        np.testing.assert_almost_equal(dd3b.npairs, dd.npairs)
+        np.testing.assert_almost_equal(dd3b.tot, dd.tot)
+        np.testing.assert_almost_equal(dd3b.xi, dd.xi)
+        np.testing.assert_almost_equal(dd3b.varxi, dd.varxi)
+        assert dd3b.coords == dd.coords
+        assert dd3b.metric == dd.metric
+        assert dd3b.sep_units == dd.sep_units
+        assert dd3b.bin_type == dd.bin_type
+
         dd4 = treecorr.NNCorrelation(bin_size=0.1, min_sep=1., max_sep=25., sep_units='arcmin')
         dd4.read(out_file_name5)
         np.testing.assert_almost_equal(dd4.logr, dd.logr)
@@ -1373,6 +1418,19 @@ def test_nn():
         assert dd4.sep_units == dd.sep_units
         assert dd4.bin_type == dd.bin_type
 
+        dd4b = treecorr.NNCorrelation.from_file(out_file_name5)
+        np.testing.assert_almost_equal(dd4b.logr, dd.logr)
+        np.testing.assert_almost_equal(dd4b.meanr, dd.meanr)
+        np.testing.assert_almost_equal(dd4b.meanlogr, dd.meanlogr)
+        np.testing.assert_almost_equal(dd4b.npairs, dd.npairs)
+        np.testing.assert_almost_equal(dd4b.tot, dd.tot)
+        assert not hasattr(dd4b,'xi')
+        assert not hasattr(dd4b,'varxi')
+        assert dd4b.coords == dd.coords
+        assert dd4b.metric == dd.metric
+        assert dd4b.sep_units == dd.sep_units
+        assert dd4b.bin_type == dd.bin_type
+
     # Check the hdf5 write option
     try:
         import h5py  # noqa: F401
@@ -1395,8 +1453,7 @@ def test_nn():
             assert 'RR' not in data
             assert 'DR' not in data
 
-        dd6 = treecorr.NNCorrelation(bin_size=0.1, min_sep=1., max_sep=25., sep_units='arcmin')
-        dd6.read(out_file_name6)
+        dd6 = treecorr.NNCorrelation.from_file(out_file_name6)
         np.testing.assert_almost_equal(dd6.logr, dd.logr)
         np.testing.assert_almost_equal(dd6.meanr, dd.meanr)
         np.testing.assert_almost_equal(dd6.meanlogr, dd.meanlogr)
diff --git a/tests/test_nnn.py b/tests/test_nnn.py
index 3d9a23b7..d4ef8cb6 100644
--- a/tests/test_nnn.py
+++ b/tests/test_nnn.py
@@ -19,7 +19,7 @@
 import time
 
 from test_helper import get_script_name, do_pickle, assert_raises, CaptureLog, timer, assert_warns
-from test_helper import is_ccw, is_ccw_3d
+from test_helper import is_ccw, is_ccw_3d, CaptureLog
 
 @timer
 def test_logruv_binning():
@@ -1367,6 +1367,21 @@ def test_direct_logruv_auto():
     np.testing.assert_allclose(ddd3.meanu, ddd.meanu)
     np.testing.assert_allclose(ddd3.meanv, ddd.meanv)
 
+    # New in version 5.0 is a simpler API for reading
+    with CaptureLog() as cl:
+        ddd3b = treecorr.NNNCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(ddd3b.ntri, ddd.ntri)
+    np.testing.assert_allclose(ddd3b.weight, ddd.weight)
+    np.testing.assert_allclose(ddd3b.meand1, ddd.meand1)
+    np.testing.assert_allclose(ddd3b.meand2, ddd.meand2)
+    np.testing.assert_allclose(ddd3b.meand3, ddd.meand3)
+    np.testing.assert_allclose(ddd3b.meanlogd1, ddd.meanlogd1)
+    np.testing.assert_allclose(ddd3b.meanlogd2, ddd.meanlogd2)
+    np.testing.assert_allclose(ddd3b.meanlogd3, ddd.meanlogd3)
+    np.testing.assert_allclose(ddd3b.meanu, ddd.meanu)
+    np.testing.assert_allclose(ddd3b.meanv, ddd.meanv)
+
     with assert_raises(TypeError):
         ddd2 += config
     ddd4 = treecorr.NNNCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins,
@@ -1457,6 +1472,18 @@ def test_direct_logruv_auto():
         np.testing.assert_allclose(ddd15.meanu, ddd.meanu)
         np.testing.assert_allclose(ddd15.meanv, ddd.meanv)
 
+        ddd15b = treecorr.NNNCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(ddd15b.ntri, ddd.ntri)
+        np.testing.assert_allclose(ddd15b.weight, ddd.weight)
+        np.testing.assert_allclose(ddd15b.meand1, ddd.meand1)
+        np.testing.assert_allclose(ddd15b.meand2, ddd.meand2)
+        np.testing.assert_allclose(ddd15b.meand3, ddd.meand3)
+        np.testing.assert_allclose(ddd15b.meanlogd1, ddd.meanlogd1)
+        np.testing.assert_allclose(ddd15b.meanlogd2, ddd.meanlogd2)
+        np.testing.assert_allclose(ddd15b.meanlogd3, ddd.meanlogd3)
+        np.testing.assert_allclose(ddd15b.meanu, ddd.meanu)
+        np.testing.assert_allclose(ddd15b.meanv, ddd.meanv)
+
     with assert_raises(ValueError):
         ddd.process(cat, algo='invalid')
     with assert_raises(ValueError):
@@ -2654,11 +2681,7 @@ def test_nnn_logruv():
         # Check the read function
         # Note: These don't need the flatten.
         # The read function should reshape them to the right shape.
-        ddd2 = treecorr.NNNCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins,
-                                       min_u=min_u, max_u=max_u, min_v=min_v, max_v=max_v,
-                                       nubins=nubins, nvbins=nvbins,
-                                       sep_units='arcmin', verbose=1, bin_type='LogRUV')
-        ddd2.read(out_file_name1)
+        ddd2 = treecorr.NNNCorrelation.from_file(out_file_name1)
         np.testing.assert_almost_equal(ddd2.logr, ddd.logr)
         np.testing.assert_almost_equal(ddd2.u, ddd.u)
         np.testing.assert_almost_equal(ddd2.v, ddd.v)
@@ -2677,7 +2700,7 @@ def test_nnn_logruv():
         assert ddd2.sep_units == ddd.sep_units
         assert ddd2.bin_type == ddd.bin_type
 
-        ddd2.read(out_file_name2)
+        ddd2 = treecorr.NNNCorrelation.from_file(out_file_name2)
         np.testing.assert_almost_equal(ddd2.logr, ddd.logr)
         np.testing.assert_almost_equal(ddd2.u, ddd.u)
         np.testing.assert_almost_equal(ddd2.v, ddd.v)
@@ -2725,11 +2748,7 @@ def test_nnn_logruv():
             attrs = data.attrs
             np.testing.assert_almost_equal(attrs['tot']/ddd.tot, 1.)
 
-        ddd3 = treecorr.NNNCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins,
-                                       min_u=min_u, max_u=max_u, min_v=min_v, max_v=max_v,
-                                       nubins=nubins, nvbins=nvbins,
-                                       sep_units='arcmin', verbose=1, bin_type='LogRUV')
-        ddd3.read(out_file_name3)
+        ddd3 = treecorr.NNNCorrelation.from_file(out_file_name3)
         np.testing.assert_almost_equal(ddd3.logr, ddd.logr)
         np.testing.assert_almost_equal(ddd3.u, ddd.u)
         np.testing.assert_almost_equal(ddd3.v, ddd.v)
@@ -2837,7 +2856,7 @@ def test_nnn_logruv():
         header = fitsio.read_header(out_file_name3, 1)
         np.testing.assert_almost_equal(header['tot']/ddd.tot, 1.)
 
-        ddd2.read(out_file_name3)
+        ddd2 = treecorr.NNNCorrelation.from_file(out_file_name3)
         np.testing.assert_almost_equal(ddd2.logr, ddd.logr)
         np.testing.assert_almost_equal(ddd2.u, ddd.u)
         np.testing.assert_almost_equal(ddd2.v, ddd.v)
@@ -3248,6 +3267,21 @@ def test_direct_logsas_auto():
     np.testing.assert_allclose(ddd3.meanlogd3, ddd.meanlogd3)
     np.testing.assert_allclose(ddd3.meanphi, ddd.meanphi)
 
+    # Check that the repr is minimal
+    assert repr(ddd3) == f"NNNCorrelation(min_sep={min_sep}, bin_size={bin_size}, nbins={nbins}, nphi_bins={nphi_bins})"
+
+    # New in version 5.0 is a simpler API for reading
+    ddd3b = treecorr.NNNCorrelation.from_file(ascii_name)
+    np.testing.assert_allclose(ddd3b.ntri, ddd.ntri)
+    np.testing.assert_allclose(ddd3b.weight, ddd.weight)
+    np.testing.assert_allclose(ddd3b.meand1, ddd.meand1)
+    np.testing.assert_allclose(ddd3b.meand2, ddd.meand2)
+    np.testing.assert_allclose(ddd3b.meand3, ddd.meand3)
+    np.testing.assert_allclose(ddd3b.meanlogd1, ddd.meanlogd1)
+    np.testing.assert_allclose(ddd3b.meanlogd2, ddd.meanlogd2)
+    np.testing.assert_allclose(ddd3b.meanlogd3, ddd.meanlogd3)
+    np.testing.assert_allclose(ddd3b.meanphi, ddd.meanphi)
+
     try:
         import fitsio
     except ImportError:
@@ -3268,6 +3302,17 @@ def test_direct_logsas_auto():
         np.testing.assert_allclose(ddd4.meanlogd3, ddd.meanlogd3)
         np.testing.assert_allclose(ddd4.meanphi, ddd.meanphi)
 
+        ddd4b = treecorr.NNNCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(ddd4b.ntri, ddd.ntri)
+        np.testing.assert_allclose(ddd4b.weight, ddd.weight)
+        np.testing.assert_allclose(ddd4b.meand1, ddd.meand1)
+        np.testing.assert_allclose(ddd4b.meand2, ddd.meand2)
+        np.testing.assert_allclose(ddd4b.meand3, ddd.meand3)
+        np.testing.assert_allclose(ddd4b.meanlogd1, ddd.meanlogd1)
+        np.testing.assert_allclose(ddd4b.meanlogd2, ddd.meanlogd2)
+        np.testing.assert_allclose(ddd4b.meanlogd3, ddd.meanlogd3)
+        np.testing.assert_allclose(ddd4b.meanphi, ddd.meanphi)
+
     # The above all used the old triangle algorithm.  Check the new default of calculating
     # LogSAS via a temporary object with LogMultipole.
     ddd2 = treecorr.NNNCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins,
@@ -3987,10 +4032,7 @@ def test_nnn_logsas():
         # Check the read function
         # Note: These don't need the flatten.
         # The read function should reshape them to the right shape.
-        ddd2 = treecorr.NNNCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins,
-                                       min_phi=min_phi, max_phi=max_phi, nphi_bins=nphi_bins,
-                                       sep_units='arcmin', bin_type='LogSAS')
-        ddd2.read(out_file_name1)
+        ddd2 = treecorr.NNNCorrelation.from_file(out_file_name1)
         np.testing.assert_almost_equal(ddd2.logd2, ddd.logd2)
         np.testing.assert_almost_equal(ddd2.logd3, ddd.logd3)
         np.testing.assert_almost_equal(ddd2.phi, ddd.phi)
@@ -4008,7 +4050,7 @@ def test_nnn_logsas():
         assert ddd2.sep_units == ddd.sep_units
         assert ddd2.bin_type == ddd.bin_type
 
-        ddd2.read(out_file_name2)
+        ddd2 = treecorr.NNNCorrelation.from_file(out_file_name2)
         np.testing.assert_almost_equal(ddd2.logd2, ddd.logd2)
         np.testing.assert_almost_equal(ddd2.logd3, ddd.logd3)
         np.testing.assert_almost_equal(ddd2.phi, ddd.phi)
@@ -4054,10 +4096,7 @@ def test_nnn_logsas():
             attrs = data.attrs
             np.testing.assert_almost_equal(attrs['tot']/ddd.tot, 1.)
 
-        ddd3 = treecorr.NNNCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins,
-                                       min_phi=min_phi, max_phi=max_phi, nphi_bins=nphi_bins,
-                                       sep_units='arcmin', bin_type='LogSAS')
-        ddd3.read(out_file_name3)
+        ddd3 = treecorr.NNNCorrelation.from_file(out_file_name3)
         np.testing.assert_almost_equal(ddd3.logd2, ddd.logd2)
         np.testing.assert_almost_equal(ddd3.logd3, ddd.logd3)
         np.testing.assert_almost_equal(ddd3.phi, ddd.phi)
@@ -4166,7 +4205,7 @@ def test_nnn_logsas():
         header = fitsio.read_header(out_file_name3, 1)
         np.testing.assert_almost_equal(header['tot']/ddd.tot, 1.)
 
-        ddd2.read(out_file_name3)
+        ddd2 = treecorr.NNNCorrelation.from_file(out_file_name3)
         np.testing.assert_almost_equal(ddd2.logd2, ddd.logd2)
         np.testing.assert_almost_equal(ddd2.logd3, ddd.logd3)
         np.testing.assert_almost_equal(ddd2.phi, ddd.phi)
@@ -4484,6 +4523,16 @@ def test_direct_logmultipole_auto():
     np.testing.assert_allclose(ddd3.meanlogd2, ddd.meanlogd2)
     np.testing.assert_allclose(ddd3.meanlogd3, ddd.meanlogd3)
 
+    ddd3b = treecorr.NNNCorrelation.from_file(ascii_name)
+    np.testing.assert_allclose(ddd3b.ntri, ddd.ntri)
+    np.testing.assert_allclose(ddd3b.weight, ddd.weight)
+    np.testing.assert_allclose(ddd3b.meand1, ddd.meand1)
+    np.testing.assert_allclose(ddd3b.meand2, ddd.meand2)
+    np.testing.assert_allclose(ddd3b.meand3, ddd.meand3)
+    np.testing.assert_allclose(ddd3b.meanlogd1, ddd.meanlogd1)
+    np.testing.assert_allclose(ddd3b.meanlogd2, ddd.meanlogd2)
+    np.testing.assert_allclose(ddd3b.meanlogd3, ddd.meanlogd3)
+
     try:
         import fitsio
     except ImportError:
@@ -4503,6 +4552,17 @@ def test_direct_logmultipole_auto():
         np.testing.assert_allclose(ddd4.meanlogd2, ddd.meanlogd2)
         np.testing.assert_allclose(ddd4.meanlogd3, ddd.meanlogd3)
 
+        ddd4b = treecorr.NNNCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(ddd4b.ntri, ddd.ntri)
+        np.testing.assert_allclose(ddd4b.weight, ddd.weight)
+        np.testing.assert_allclose(ddd4b.meand1, ddd.meand1)
+        np.testing.assert_allclose(ddd4b.meand2, ddd.meand2)
+        np.testing.assert_allclose(ddd4b.meand3, ddd.meand3)
+        np.testing.assert_allclose(ddd4b.meanlogd1, ddd.meanlogd1)
+        np.testing.assert_allclose(ddd4b.meanlogd2, ddd.meanlogd2)
+        np.testing.assert_allclose(ddd4b.meanlogd3, ddd.meanlogd3)
+
+
 @timer
 def test_direct_logmultipole_cross12():
     # Check the 1-2 cross correlation with LogMultipole
@@ -4728,9 +4788,7 @@ def test_direct_logmultipole_cross12():
     ddd.process(cat1, cat1.patches[0])
     file_name = os.path.join('data','nnn_zero_copy.dat')
     ddd.write(file_name, precision=16, write_patch_results=True)
-    ddd2 = treecorr.NNNCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, max_n=max_n,
-                                   bin_type='LogMultipole')
-    ddd2.read(file_name)
+    ddd2 = treecorr.NNNCorrelation.from_file(file_name)
     assert len(ddd2.results) == len(ddd.results)
     for key in ddd.results:
         np.testing.assert_allclose(ddd2.results[key].weight, ddd.results[key].weight)
diff --git a/tests/test_nq.py b/tests/test_nq.py
index 48224b37..5c79f81c 100644
--- a/tests/test_nq.py
+++ b/tests/test_nq.py
@@ -156,7 +156,7 @@ def test_direct():
 
     ascii_name = 'output/nq_ascii.txt'
     nq.write(ascii_name, precision=16)
-    nq3 = treecorr.NQCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    nq3 = treecorr.NQCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     nq3.read(ascii_name)
     np.testing.assert_allclose(nq3.npairs, nq.npairs)
     np.testing.assert_allclose(nq3.weight, nq.weight)
@@ -165,6 +165,20 @@ def test_direct():
     np.testing.assert_allclose(nq3.xi, nq.xi)
     np.testing.assert_allclose(nq3.xi_im, nq.xi_im)
 
+    # Check that the repr is minimal
+    assert repr(nq3) == f'NQCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # Simpler API using from_file:
+    with CaptureLog() as cl:
+        nq3b = treecorr.NQCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(nq3b.npairs, nq.npairs)
+    np.testing.assert_allclose(nq3b.weight, nq.weight)
+    np.testing.assert_allclose(nq3b.meanr, nq.meanr)
+    np.testing.assert_allclose(nq3b.meanlogr, nq.meanlogr)
+    np.testing.assert_allclose(nq3b.xi, nq.xi)
+    np.testing.assert_allclose(nq3b.xi_im, nq.xi_im)
+
     try:
         import fitsio
     except ImportError:
@@ -181,6 +195,14 @@ def test_direct():
         np.testing.assert_allclose(nq4.xi, nq.xi)
         np.testing.assert_allclose(nq4.xi_im, nq.xi_im)
 
+        nq4b = treecorr.NQCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(nq4b.npairs, nq.npairs)
+        np.testing.assert_allclose(nq4b.weight, nq.weight)
+        np.testing.assert_allclose(nq4b.meanr, nq.meanr)
+        np.testing.assert_allclose(nq4b.meanlogr, nq.meanlogr)
+        np.testing.assert_allclose(nq4b.xi, nq.xi)
+        np.testing.assert_allclose(nq4b.xi_im, nq.xi_im)
+
     with assert_raises(TypeError):
         nq2 += config
     nq4 = treecorr.NQCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -665,8 +687,7 @@ def test_nq():
         np.testing.assert_almost_equal(data['npairs'], nq.npairs)
 
         # Check the read function
-        nq2 = treecorr.NQCorrelation(bin_size=0.1, min_sep=1., max_sep=20., sep_units='arcmin')
-        nq2.read(out_file_name2)
+        nq2 = treecorr.NQCorrelation.from_file(out_file_name2)
         np.testing.assert_almost_equal(nq2.logr, nq.logr)
         np.testing.assert_almost_equal(nq2.meanr, nq.meanr)
         np.testing.assert_almost_equal(nq2.meanlogr, nq.meanlogr)
@@ -1152,8 +1173,7 @@ def make_spin4_field(rng):
     else:
         file_name = os.path.join('output','test_write_results_nq.fits')
         nq2.write(file_name, write_patch_results=True)
-        nq5 = treecorr.NQCorrelation(corr_params)
-        nq5.read(file_name)
+        nq5 = treecorr.NQCorrelation.from_file(file_name)
         cov5 = nq5.estimate_cov('jackknife')
         np.testing.assert_allclose(cov5, cov2)
 
diff --git a/tests/test_nt.py b/tests/test_nt.py
index e4b2b7a2..76105f21 100644
--- a/tests/test_nt.py
+++ b/tests/test_nt.py
@@ -156,7 +156,7 @@ def test_direct():
 
     ascii_name = 'output/nt_ascii.txt'
     nt.write(ascii_name, precision=16)
-    nt3 = treecorr.NTCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    nt3 = treecorr.NTCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     nt3.read(ascii_name)
     np.testing.assert_allclose(nt3.npairs, nt.npairs)
     np.testing.assert_allclose(nt3.weight, nt.weight)
@@ -165,6 +165,20 @@ def test_direct():
     np.testing.assert_allclose(nt3.xi, nt.xi)
     np.testing.assert_allclose(nt3.xi_im, nt.xi_im)
 
+    # Check that the repr is minimal
+    assert repr(nt3) == f'NTCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # Simpler API using from_file:
+    with CaptureLog() as cl:
+        nt3b = treecorr.NTCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(nt3b.npairs, nt.npairs)
+    np.testing.assert_allclose(nt3b.weight, nt.weight)
+    np.testing.assert_allclose(nt3b.meanr, nt.meanr)
+    np.testing.assert_allclose(nt3b.meanlogr, nt.meanlogr)
+    np.testing.assert_allclose(nt3b.xi, nt.xi)
+    np.testing.assert_allclose(nt3b.xi_im, nt.xi_im)
+
     try:
         import fitsio
     except ImportError:
@@ -181,6 +195,14 @@ def test_direct():
         np.testing.assert_allclose(nt4.xi, nt.xi)
         np.testing.assert_allclose(nt4.xi_im, nt.xi_im)
 
+        nt4b = treecorr.NTCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(nt4b.npairs, nt.npairs)
+        np.testing.assert_allclose(nt4b.weight, nt.weight)
+        np.testing.assert_allclose(nt4b.meanr, nt.meanr)
+        np.testing.assert_allclose(nt4b.meanlogr, nt.meanlogr)
+        np.testing.assert_allclose(nt4b.xi, nt.xi)
+        np.testing.assert_allclose(nt4b.xi_im, nt.xi_im)
+
     with assert_raises(TypeError):
         nt2 += config
     nt4 = treecorr.NTCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -670,8 +692,7 @@ def test_nt():
         np.testing.assert_almost_equal(data['npairs'], nt.npairs)
 
         # Check the read function
-        nt2 = treecorr.NTCorrelation(bin_size=0.1, min_sep=1., max_sep=20., sep_units='arcmin')
-        nt2.read(out_file_name2)
+        nt2 = treecorr.NTCorrelation.from_file(out_file_name2)
         np.testing.assert_almost_equal(nt2.logr, nt.logr)
         np.testing.assert_almost_equal(nt2.meanr, nt.meanr)
         np.testing.assert_almost_equal(nt2.meanlogr, nt.meanlogr)
@@ -1131,8 +1152,7 @@ def make_spin3_field(rng):
     else:
         file_name = os.path.join('output','test_write_results_nt.fits')
         nt2.write(file_name, write_patch_results=True)
-        nt5 = treecorr.NTCorrelation(corr_params)
-        nt5.read(file_name)
+        nt5 = treecorr.NTCorrelation.from_file(file_name)
         cov5 = nt5.estimate_cov('jackknife')
         np.testing.assert_allclose(cov5, cov2)
 
diff --git a/tests/test_nv.py b/tests/test_nv.py
index 83d71ce3..bdc32b0c 100644
--- a/tests/test_nv.py
+++ b/tests/test_nv.py
@@ -156,7 +156,7 @@ def test_direct():
 
     ascii_name = 'output/nv_ascii.txt'
     nv.write(ascii_name, precision=16)
-    nv3 = treecorr.NVCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    nv3 = treecorr.NVCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     nv3.read(ascii_name)
     np.testing.assert_allclose(nv3.npairs, nv.npairs)
     np.testing.assert_allclose(nv3.weight, nv.weight)
@@ -165,6 +165,20 @@ def test_direct():
     np.testing.assert_allclose(nv3.xi, nv.xi)
     np.testing.assert_allclose(nv3.xi_im, nv.xi_im)
 
+    # Check that the repr is minimal
+    assert repr(nv3) == f'NVCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # Simpler API using from_file:
+    with CaptureLog() as cl:
+        nv3b = treecorr.NVCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(nv3b.npairs, nv.npairs)
+    np.testing.assert_allclose(nv3b.weight, nv.weight)
+    np.testing.assert_allclose(nv3b.meanr, nv.meanr)
+    np.testing.assert_allclose(nv3b.meanlogr, nv.meanlogr)
+    np.testing.assert_allclose(nv3b.xi, nv.xi)
+    np.testing.assert_allclose(nv3b.xi_im, nv.xi_im)
+
     try:
         import fitsio
     except ImportError:
@@ -181,6 +195,14 @@ def test_direct():
         np.testing.assert_allclose(nv4.xi, nv.xi)
         np.testing.assert_allclose(nv4.xi_im, nv.xi_im)
 
+        nv4b = treecorr.NVCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(nv4b.npairs, nv.npairs)
+        np.testing.assert_allclose(nv4b.weight, nv.weight)
+        np.testing.assert_allclose(nv4b.meanr, nv.meanr)
+        np.testing.assert_allclose(nv4b.meanlogr, nv.meanlogr)
+        np.testing.assert_allclose(nv4b.xi, nv.xi)
+        np.testing.assert_allclose(nv4b.xi_im, nv.xi_im)
+
     with assert_raises(TypeError):
         nv2 += config
     nv4 = treecorr.NVCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -667,8 +689,7 @@ def test_nv():
         np.testing.assert_almost_equal(data['npairs'], nv.npairs)
 
         # Check the read function
-        nv2 = treecorr.NVCorrelation(bin_size=0.1, min_sep=1., max_sep=20., sep_units='arcmin')
-        nv2.read(out_file_name2)
+        nv2 = treecorr.NVCorrelation.from_file(out_file_name2)
         np.testing.assert_almost_equal(nv2.logr, nv.logr)
         np.testing.assert_almost_equal(nv2.meanr, nv.meanr)
         np.testing.assert_almost_equal(nv2.meanlogr, nv.meanlogr)
@@ -1150,8 +1171,7 @@ def make_velocity_field(rng):
     else:
         file_name = os.path.join('output','test_write_results_nv.fits')
         nv2.write(file_name, write_patch_results=True)
-        nv5 = treecorr.NVCorrelation(corr_params)
-        nv5.read(file_name)
+        nv5 = treecorr.NVCorrelation.from_file(file_name)
         cov5 = nv5.estimate_cov('jackknife')
         np.testing.assert_allclose(cov5, cov2)
 
diff --git a/tests/test_patch.py b/tests/test_patch.py
index 3554f481..c35a6c0c 100644
--- a/tests/test_patch.py
+++ b/tests/test_patch.py
@@ -907,23 +907,30 @@ def test_gg_jk():
     else:
         file_name = os.path.join('output','test_write_results_gg.fits')
         gg3.write(file_name, write_patch_results=True)
-        gg4 = treecorr.GGCorrelation(bin_size=0.3, min_sep=10., max_sep=50.)
-        gg4.read(file_name)
+        gg4 = treecorr.GGCorrelation.from_file(file_name)
         cov4 = gg4.estimate_cov('jackknife')
         np.testing.assert_allclose(cov4, gg3.cov)
         covxip4 = gg4.estimate_cov('jackknife', func=lambda gg: gg.xip)
         np.testing.assert_allclose(covxip4, covxip)
 
+        # With write_cov=True, cov is serialized as well.
+        gg3.write(file_name, write_patch_results=True, write_cov=True)
+        gg4 = treecorr.GGCorrelation.from_file(file_name)
+        np.testing.assert_allclose(gg4.cov, gg3.cov)
+
     # Also with ascii, since that works differeny.
     file_name = os.path.join('output','test_write_results_gg.dat')
     gg3.write(file_name, write_patch_results=True)
-    gg5 = treecorr.GGCorrelation(bin_size=0.3, min_sep=10., max_sep=50.)
-    gg5.read(file_name)
+    gg5 = treecorr.GGCorrelation.from_file(file_name)
     cov5 = gg5.estimate_cov('jackknife')
     np.testing.assert_allclose(cov5, gg3.cov)
     covxip5 = gg5.estimate_cov('jackknife', func=lambda gg: gg.xip)
     np.testing.assert_allclose(covxip5, covxip)
 
+    gg3.write(file_name, write_patch_results=True, write_cov=True)
+    gg5 = treecorr.GGCorrelation.from_file(file_name)
+    np.testing.assert_allclose(gg5.cov, gg3.cov)
+
     # And also try to match the type if HDF
     try:
         import h5py
@@ -933,13 +940,16 @@ def test_gg_jk():
         # Finally with hdf
         file_name = os.path.join('output','test_write_results_gg.hdf5')
         gg3.write(file_name, write_patch_results=True)
-        gg6 = treecorr.GGCorrelation(bin_size=0.3, min_sep=10., max_sep=50.)
-        gg6.read(file_name)
+        gg6 = treecorr.GGCorrelation.from_file(file_name)
         cov6 = gg6.estimate_cov('jackknife')
         np.testing.assert_allclose(cov6, gg3.cov)
         covxip6 = gg6.estimate_cov('jackknife', func=lambda gg: gg.xip)
         np.testing.assert_allclose(covxip6, covxip)
 
+        gg3.write(file_name, write_cov=True)
+        gg6 = treecorr.GGCorrelation.from_file(file_name)
+        np.testing.assert_allclose(gg6.cov, gg3.cov)
+
     # Check some invalid actions
     # Bad var_method
     with assert_raises(ValueError):
@@ -1225,23 +1235,26 @@ def test_ng_jk():
     else:
         file_name = os.path.join('output','test_write_results_ng.fits')
         ng3.write(file_name, write_patch_results=True)
-        ng4 = treecorr.NGCorrelation(bin_size=0.3, min_sep=10., max_sep=50.)
-        ng4.read(file_name)
-        print('ng4.xi = ',ng4.xi)
-        print('results = ',ng4.results)
-        print('results = ',ng4.results.keys())
+        ng4 = treecorr.NGCorrelation.from_file(file_name)
         cov4 = ng4.estimate_cov('jackknife')
-        print('cov4 = ',cov4)
         np.testing.assert_allclose(cov4, ng3.cov)
 
+        # With write_cov=True, cov is serialized as well.
+        ng3.write(file_name, write_patch_results=True, write_cov=True)
+        ng4 = treecorr.NGCorrelation.from_file(file_name)
+        np.testing.assert_allclose(ng4.cov, ng3.cov)
+
     # Also with ascii, since that works differeny.
     file_name = os.path.join('output','test_write_results_ng.dat')
     ng3.write(file_name, write_patch_results=True)
-    ng5 = treecorr.NGCorrelation(bin_size=0.3, min_sep=10., max_sep=50.)
-    ng5.read(file_name)
+    ng5 = treecorr.NGCorrelation.from_file(file_name)
     cov5 = ng5.estimate_cov('jackknife')
     np.testing.assert_allclose(cov5, ng3.cov)
 
+    ng3.write(file_name, write_patch_results=True, write_cov=True)
+    ng5 = treecorr.NGCorrelation.from_file(file_name)
+    np.testing.assert_allclose(ng5.cov, ng3.cov)
+
     # And also try to match the type if HDF
     try:
         import h5py
@@ -1251,11 +1264,14 @@ def test_ng_jk():
         # Finally with hdf
         file_name = os.path.join('output','test_write_results_ng.hdf5')
         ng3.write(file_name, write_patch_results=True)
-        ng6 = treecorr.NGCorrelation(bin_size=0.3, min_sep=10., max_sep=50.)
-        ng6.read(file_name)
+        ng6 = treecorr.NGCorrelation.from_file(file_name)
         cov6 = ng6.estimate_cov('jackknife')
         np.testing.assert_allclose(cov6, ng3.cov)
 
+        ng3.write(file_name, write_patch_results=True, write_cov=True)
+        ng6 = treecorr.NGCorrelation.from_file(file_name)
+        np.testing.assert_allclose(ng6.cov, ng3.cov)
+
     # Use a random catalog
     # In this case the locations of the source catalog are fine to use as our random catalog,
     # since they fill the region where the lenses are allowed to be.
@@ -1543,16 +1559,23 @@ def test_nn_jk():
         nn3.write(file_name, write_patch_results=True)
         rr.write(rr_file_name, write_patch_results=True)
         nr3.write(dr_file_name, write_patch_results=True)
-        nn4 = treecorr.NNCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
-        rr4 = treecorr.NNCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
-        nr4 = treecorr.NNCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
-        nn4.read(file_name)
-        rr4.read(rr_file_name)
-        nr4.read(dr_file_name)
+        nn4 = treecorr.NNCorrelation.from_file(file_name)
+        rr4 = treecorr.NNCorrelation.from_file(rr_file_name)
+        nr4 = treecorr.NNCorrelation.from_file(dr_file_name)
         nn4.calculateXi(rr=rr4, dr=nr4)
         cov4 = nn4.estimate_cov('jackknife')
         np.testing.assert_allclose(cov4, nn3.cov)
 
+        # It should also work (now, as of version 5.0) from just the nn output file.
+        nn4 = treecorr.NNCorrelation.from_file(file_name)
+        cov4 = nn4.estimate_cov('jackknife')
+        np.testing.assert_allclose(cov4, nn3.cov)
+
+        # With write_cov=True, cov is serialized as well.
+        nn3.write(file_name, write_patch_results=True, write_cov=True)
+        nn4 = treecorr.NNCorrelation.from_file(file_name)
+        np.testing.assert_allclose(nn4.cov, nn3.cov)
+
     # Also with ascii, since that works differeny.
     file_name = os.path.join('output','test_write_results_dd.dat')
     rr_file_name = os.path.join('output','test_write_results_rr.dat')
@@ -1560,16 +1583,21 @@ def test_nn_jk():
     nn3.write(file_name, write_patch_results=True, precision=15)
     rr.write(rr_file_name, write_patch_results=True, precision=15)
     nr3.write(dr_file_name, write_patch_results=True, precision=15)
-    nn5 = treecorr.NNCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
-    rr5 = treecorr.NNCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
-    dr5 = treecorr.NNCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
-    nn5.read(file_name)
-    rr5.read(rr_file_name)
-    dr5.read(dr_file_name)
+    nn5 = treecorr.NNCorrelation.from_file(file_name)
+    rr5 = treecorr.NNCorrelation.from_file(rr_file_name)
+    dr5 = treecorr.NNCorrelation.from_file(dr_file_name)
     nn5.calculateXi(rr=rr5, dr=dr5)
     cov5 = nn5.estimate_cov('jackknife')
     np.testing.assert_allclose(cov5, nn3.cov)
 
+    nn5 = treecorr.NNCorrelation.from_file(file_name)
+    cov5 = nn5.estimate_cov('jackknife')
+    np.testing.assert_allclose(cov5, nn3.cov)
+
+    nn3.write(file_name, write_patch_results=True, write_cov=True)
+    nn5 = treecorr.NNCorrelation.from_file(file_name)
+    np.testing.assert_allclose(nn5.cov, nn3.cov)
+
     # And also try to match the type if HDF
     try:
         import h5py
@@ -1583,16 +1611,21 @@ def test_nn_jk():
         nn3.write(file_name, write_patch_results=True)
         rr.write(rr_file_name, write_patch_results=True)
         nr3.write(dr_file_name, write_patch_results=True)
-        nn6 = treecorr.NNCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
-        rr6 = treecorr.NNCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
-        dr6 = treecorr.NNCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
-        nn6.read(file_name)
-        rr6.read(rr_file_name)
-        dr6.read(dr_file_name)
+        nn6 = treecorr.NNCorrelation.from_file(file_name)
+        rr6 = treecorr.NNCorrelation.from_file(rr_file_name)
+        dr6 = treecorr.NNCorrelation.from_file(dr_file_name)
         nn6.calculateXi(rr=rr6, dr=dr6)
         cov6 = nn6.estimate_cov('jackknife')
         np.testing.assert_allclose(cov6, nn3.cov)
 
+        nn6 = treecorr.NNCorrelation.from_file(file_name)
+        cov6 = nn6.estimate_cov('jackknife')
+        np.testing.assert_allclose(cov6, nn3.cov)
+
+        nn3.write(file_name, write_patch_results=True, write_cov=True)
+        nn6 = treecorr.NNCorrelation.from_file(file_name)
+        np.testing.assert_allclose(nn6.cov, nn3.cov)
+
     # Check NN cross-correlation and other combinations of dr, rd.
     rn3 = treecorr.NNCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
     t0 = time.time()
@@ -1660,6 +1693,14 @@ def test_nn_jk():
     np.testing.assert_allclose(varxic4, varxib4)
     np.testing.assert_allclose(varxid4, varxib4)
 
+    # Repeat the serialization check, now that we have all 4 dd, rr, dr, rd
+    # Just do ASCII this time, since that's sufficient for this.
+    file_name = os.path.join('output','test_write_results.out')
+    nn4.write(file_name, write_patch_results=True, precision=15)
+    nn5 = treecorr.NNCorrelation.from_file(file_name)
+    cov5 = nn5.estimate_cov('jackknife')
+    np.testing.assert_allclose(cov5, nn4.cov)
+
     # Check with patch_method='local'
     print('with patch_method=local:')
     nn4.process(catp, patch_method='local')
@@ -1830,19 +1871,26 @@ def test_kappa_jk():
     else:
         file_name = os.path.join('output','test_write_results_nk.fits')
         nk.write(file_name, write_patch_results=True)
-        nk4 = treecorr.NKCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
-        nk4.read(file_name)
+        nk4 = treecorr.NKCorrelation.from_file(file_name)
         cov4 = nk4.estimate_cov('jackknife')
         np.testing.assert_allclose(cov4, nk.cov)
 
+        # With write_cov=True, cov is serialized as well.
+        nk.write(file_name, write_patch_results=True, write_cov=True)
+        nk4 = treecorr.NKCorrelation.from_file(file_name)
+        np.testing.assert_allclose(nk4.cov, nk.cov)
+
     # Also with ascii, since that works differeny.
     file_name = os.path.join('output','test_write_results_nk.dat')
     nk.write(file_name, write_patch_results=True)
-    nk5 = treecorr.NKCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
-    nk5.read(file_name)
+    nk5 = treecorr.NKCorrelation.from_file(file_name)
     cov5 = nk5.estimate_cov('jackknife')
     np.testing.assert_allclose(cov5, nk.cov)
 
+    nk.write(file_name, write_patch_results=True, write_cov=True)
+    nk5 = treecorr.NKCorrelation.from_file(file_name)
+    np.testing.assert_allclose(nk5.cov, nk.cov)
+
     # And also try to match the type if HDF
     try:
         import h5py
@@ -1852,11 +1900,14 @@ def test_kappa_jk():
         # Finally with hdf
         file_name = os.path.join('output','test_write_results_nk.hdf5')
         nk.write(file_name, write_patch_results=True)
-        nk6 = treecorr.NKCorrelation(bin_size=0.3, min_sep=10., max_sep=30.)
-        nk6.read(file_name)
+        nk6 = treecorr.NKCorrelation.from_file(file_name)
         cov6 = nk6.estimate_cov('jackknife')
         np.testing.assert_allclose(cov6, nk.cov)
 
+        nk.write(file_name, write_patch_results=True, write_cov=True)
+        nk6 = treecorr.NKCorrelation.from_file(file_name)
+        np.testing.assert_allclose(nk6.cov, nk.cov)
+
     # Use a random catalog
     # In this case the locations of the source catalog are fine to use as our random catalog,
     # since they fill the region where the lenses are allowed to be.
@@ -1920,19 +1971,25 @@ def test_kappa_jk():
     else:
         file_name = os.path.join('output','test_write_results_kk.fits')
         kk.write(file_name, write_patch_results=True)
-        kk4 = treecorr.KKCorrelation(bin_size=0.3, min_sep=6., max_sep=30.)
-        kk4.read(file_name)
+        kk4 = treecorr.KKCorrelation.from_file(file_name)
         cov4 = kk4.estimate_cov('jackknife')
         np.testing.assert_allclose(cov4, kk.cov)
 
+        kk.write(file_name, write_patch_results=True, write_cov=True)
+        kk4 = treecorr.KKCorrelation.from_file(file_name)
+        np.testing.assert_allclose(kk4.cov, kk.cov)
+
     # Also with ascii, since that works differeny.
     file_name = os.path.join('output','test_write_results_kk.dat')
     kk.write(file_name, write_patch_results=True)
-    kk5 = treecorr.KKCorrelation(bin_size=0.3, min_sep=6., max_sep=30.)
-    kk5.read(file_name)
+    kk5 = treecorr.KKCorrelation.from_file(file_name)
     cov5 = kk5.estimate_cov('jackknife')
     np.testing.assert_allclose(cov5, kk.cov)
 
+    kk.write(file_name, write_patch_results=True, write_cov=True)
+    kk5 = treecorr.KKCorrelation.from_file(file_name)
+    np.testing.assert_allclose(kk5.cov, kk.cov)
+
     # And also try to match the type if HDF
     try:
         import h5py
@@ -1942,11 +1999,14 @@ def test_kappa_jk():
         # Finally with hdf
         file_name = os.path.join('output','test_write_results_kk.hdf5')
         kk.write(file_name, write_patch_results=True)
-        kk6 = treecorr.KKCorrelation(bin_size=0.3, min_sep=6., max_sep=30.)
-        kk6.read(file_name)
+        kk6 = treecorr.KKCorrelation.from_file(file_name)
         cov6 = kk6.estimate_cov('jackknife')
         np.testing.assert_allclose(cov6, kk.cov)
 
+        kk.write(file_name, write_patch_results=True, write_cov=True)
+        kk6 = treecorr.KKCorrelation.from_file(file_name)
+        np.testing.assert_allclose(kk6.cov, kk.cov)
+
     # KG
     # Same scales as we used for NG, which works fine with kappa as the "lens" too.
     kg = treecorr.KGCorrelation(bin_size=0.3, min_sep=10, max_sep=50., var_method='jackknife')
@@ -1976,19 +2036,25 @@ def test_kappa_jk():
     else:
         file_name = os.path.join('output','test_write_results_kg.fits')
         kg.write(file_name, write_patch_results=True)
-        kg4 = treecorr.KGCorrelation(bin_size=0.3, min_sep=10., max_sep=50.)
-        kg4.read(file_name)
+        kg4 = treecorr.KGCorrelation.from_file(file_name)
         cov4 = kg4.estimate_cov('jackknife')
         np.testing.assert_allclose(cov4, kg.cov)
 
+        kg.write(file_name, write_patch_results=True, write_cov=True)
+        kg4 = treecorr.KGCorrelation.from_file(file_name)
+        np.testing.assert_allclose(kg4.cov, kg.cov)
+
     # Also with ascii, since that works differeny.
     file_name = os.path.join('output','test_write_results_kg.dat')
     kg.write(file_name, write_patch_results=True)
-    kg5 = treecorr.KGCorrelation(bin_size=0.3, min_sep=10., max_sep=50.)
-    kg5.read(file_name)
+    kg5 = treecorr.KGCorrelation.from_file(file_name)
     cov5 = kg5.estimate_cov('jackknife')
     np.testing.assert_allclose(cov5, kg.cov)
 
+    kg.write(file_name, write_patch_results=True, write_cov=True)
+    kg5 = treecorr.KGCorrelation.from_file(file_name)
+    np.testing.assert_allclose(kg5.cov, kg.cov)
+
     # And also try to match the type if HDF
     try:
         import h5py
@@ -1998,11 +2064,14 @@ def test_kappa_jk():
         # Finally with hdf
         file_name = os.path.join('output','test_write_results_kg.hdf5')
         kg.write(file_name, write_patch_results=True)
-        kg6 = treecorr.KGCorrelation(bin_size=0.3, min_sep=10., max_sep=50.)
-        kg6.read(file_name)
+        kg6 = treecorr.KGCorrelation.from_file(file_name)
         cov6 = kg6.estimate_cov('jackknife')
         np.testing.assert_allclose(cov6, kg.cov)
 
+        kg.write(file_name, write_patch_results=True, write_cov=True)
+        kg6 = treecorr.KGCorrelation.from_file(file_name)
+        np.testing.assert_allclose(kg6.cov, kg.cov)
+
     # Do a real multi-statistic covariance.
     t0 = time.time()
     cov = treecorr.estimate_multi_cov([nk,kk,kg], 'jackknife')
@@ -3007,8 +3076,7 @@ def test_config():
 
     # Check that we get the same result using the corr2 function
     treecorr.corr2(config, logger=logger)
-    gg2 = treecorr.GGCorrelation(config)
-    gg2.read(config['gg_file_name'])
+    gg2 = treecorr.GGCorrelation.from_file(config['gg_file_name'])
     print('gg.varxip = ',gg.varxip)
     print('gg2.varxip = ',gg2.varxip)
 
diff --git a/tests/test_patch3pt.py b/tests/test_patch3pt.py
index 618b369f..b7e45be1 100644
--- a/tests/test_patch3pt.py
+++ b/tests/test_patch3pt.py
@@ -249,6 +249,11 @@ def test_kkk_logruv_jk():
         cov3 = kkk3.estimate_cov('jackknife')
         np.testing.assert_allclose(cov3, cov1)
 
+        # With write_cov=True, cov is serialized as well.
+        kkkp.write(file_name, write_patch_results=True, write_cov=True)
+        kkk3 = treecorr.KKKCorrelation.from_file(file_name)
+        np.testing.assert_allclose(kkk3.cov, kkkp.cov)
+
     # Also with ascii, since that works differeny.
     file_name = os.path.join('output','test_write_results_kkk.dat')
     kkkp.write(file_name, write_patch_results=True)
@@ -259,6 +264,10 @@ def test_kkk_logruv_jk():
     cov4 = kkk4.estimate_cov('jackknife')
     np.testing.assert_allclose(cov4, cov1)
 
+    kkkp.write(file_name, write_patch_results=True, write_cov=True)
+    kkk4 = treecorr.KKKCorrelation.from_file(file_name)
+    np.testing.assert_allclose(kkk4.cov, kkkp.cov)
+
     # And also try to match the type if HDF
     try:
         import h5py
@@ -277,6 +286,10 @@ def test_kkk_logruv_jk():
         cov5 = kkk5.estimate_cov('jackknife')
         np.testing.assert_allclose(cov5, cov1)
 
+        kkkp.write(file_name, write_patch_results=True, write_cov=True)
+        kkk5 = treecorr.KKKCorrelation.from_file(file_name)
+        np.testing.assert_allclose(kkk5.cov, kkkp.cov)
+
     # Now as a cross correlation with all 3 using the same patch catalog.
     print('with 3 patched catalogs:')
     kkkp.process(catp, catp, catp)
@@ -772,6 +785,10 @@ def test_ggg_logruv_jk():
         print('cov3 = ',cov3)
         np.testing.assert_allclose(cov3, cov1)
 
+        gggp.write(file_name, write_patch_results=True, write_cov=True)
+        ggg3 = treecorr.GGGCorrelation.from_file(file_name)
+        np.testing.assert_allclose(ggg3.cov, gggp.cov)
+
     # Also with ascii, since that works differeny.
     file_name = os.path.join('output','test_write_results_ggg.dat')
     gggp.write(file_name, write_patch_results=True)
@@ -782,6 +799,10 @@ def test_ggg_logruv_jk():
     cov4 = ggg4.estimate_cov('jackknife', func=f)
     np.testing.assert_allclose(cov4, cov1)
 
+    gggp.write(file_name, write_patch_results=True, write_cov=True)
+    ggg4 = treecorr.GGGCorrelation.from_file(file_name)
+    np.testing.assert_allclose(ggg4.cov, gggp.cov)
+
     # And also try to match the type if HDF
     try:
         import h5py
@@ -800,6 +821,10 @@ def test_ggg_logruv_jk():
         cov5 = ggg5.estimate_cov('jackknife', func=f)
         np.testing.assert_allclose(cov5, cov1)
 
+        gggp.write(file_name, write_patch_results=True, write_cov=True)
+        ggg5 = treecorr.GGGCorrelation.from_file(file_name)
+        np.testing.assert_allclose(ggg5.cov, gggp.cov)
+
     # Now as a cross correlation with all 3 using the same patch catalog.
     print('with 3 patched catalogs:')
     gggp.process(catp, catp, catp)
@@ -1499,6 +1524,16 @@ def test_nnn_logruv_jk():
         cov3 = ddd3.estimate_cov('jackknife')
         np.testing.assert_allclose(cov3, cov1)
 
+        # It should also work (now, as of version 5.0) from just the ddd output file.
+        ddd3b = treecorr.NNNCorrelation.from_file(file_name)
+        cov3b = ddd3b.estimate_cov('jackknife')
+        np.testing.assert_allclose(cov3b, cov1)
+
+        # And with write_cov=True, the covariance matrix is serialized as well.
+        dddp.write(file_name, write_patch_results=True, write_cov=True)
+        ddd3c = treecorr.NNNCorrelation.from_file(file_name)
+        np.testing.assert_allclose(ddd3c.cov, dddp.cov)
+
     # Also with ascii, since that works differeny.
     file_name = os.path.join('output','test_write_results_ddd.dat')
     rrr_file_name = os.path.join('output','test_write_results_rrr.dat')
@@ -1508,20 +1543,23 @@ def test_nnn_logruv_jk():
     rrrp.write(rrr_file_name, write_patch_results=True)
     drrp.write(drr_file_name, write_patch_results=True)
     rddp.write(rdd_file_name, write_patch_results=True)
-    ddd4 = treecorr.NNNCorrelation(nbins=3, min_sep=50., max_sep=100., bin_slop=0.2,
-                                   min_u=0.8, max_u=1.0, nubins=1,
-                                   min_v=0.0, max_v=0.2, nvbins=1, bin_type='LogRUV')
-    rrr4 = ddd4.copy()
-    drr4 = ddd4.copy()
-    rdd4 = ddd4.copy()
-    ddd4.read(file_name)
-    rrr4.read(rrr_file_name)
-    drr4.read(drr_file_name)
-    rdd4.read(rdd_file_name)
+    ddd4 = treecorr.NNNCorrelation.from_file(file_name)
+    rrr4 = treecorr.NNNCorrelation.from_file(rrr_file_name)
+    drr4 = treecorr.NNNCorrelation.from_file(drr_file_name)
+    rdd4 = treecorr.NNNCorrelation.from_file(rdd_file_name)
     ddd4.calculateZeta(rrr=rrr4, drr=drr4, rdd=rdd4)
     cov4 = ddd4.estimate_cov('jackknife')
     np.testing.assert_allclose(cov4, cov1)
 
+    # It should also work (now, as of version 5.0) from just the ddd output file.
+    ddd4b = treecorr.NNNCorrelation.from_file(file_name)
+    cov4b = ddd4b.estimate_cov('jackknife')
+    np.testing.assert_allclose(cov4b, cov1)
+
+    dddp.write(file_name, write_patch_results=True, write_cov=True)
+    ddd4c = treecorr.NNNCorrelation.from_file(file_name)
+    np.testing.assert_allclose(ddd4c.cov, dddp.cov)
+
     # And also try to match the type if HDF
     try:
         import h5py
@@ -1553,6 +1591,14 @@ def test_nnn_logruv_jk():
         cov5 = ddd5.estimate_cov('jackknife')
         np.testing.assert_allclose(cov5, cov1)
 
+        ddd5b = treecorr.NNNCorrelation.from_file(file_name)
+        cov5b = ddd5b.estimate_cov('jackknife')
+        np.testing.assert_allclose(cov5b, cov1)
+
+        dddp.write(file_name, write_patch_results=True, write_cov=True)
+        ddd5c = treecorr.NNNCorrelation.from_file(file_name)
+        np.testing.assert_allclose(ddd5c.cov, dddp.cov)
+
     # Don't check LogSAS for accuracy.  Just make sure the code runs.
     print('LogSAS')
     dddp2 = treecorr.NNNCorrelation(nbins=3, min_sep=50., max_sep=100., bin_slop=0.2,
diff --git a/tests/test_qq.py b/tests/test_qq.py
index 50114941..9a391eb5 100644
--- a/tests/test_qq.py
+++ b/tests/test_qq.py
@@ -171,7 +171,7 @@ def test_direct():
 
     ascii_name = 'output/qq_ascii.txt'
     qq.write(ascii_name, precision=16)
-    qq3 = treecorr.QQCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    qq3 = treecorr.QQCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     qq3.read(ascii_name)
     np.testing.assert_allclose(qq3.npairs, qq.npairs)
     np.testing.assert_allclose(qq3.weight, qq.weight)
@@ -182,6 +182,22 @@ def test_direct():
     np.testing.assert_allclose(qq3.xim, qq.xim)
     np.testing.assert_allclose(qq3.xim_im, qq.xim_im)
 
+    # Check that the repr is minimal
+    assert repr(qq3) == f'QQCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # Simpler API using from_file:
+    with CaptureLog() as cl:
+        qq3b = treecorr.QQCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(qq3b.npairs, qq.npairs)
+    np.testing.assert_allclose(qq3b.weight, qq.weight)
+    np.testing.assert_allclose(qq3b.meanr, qq.meanr)
+    np.testing.assert_allclose(qq3b.meanlogr, qq.meanlogr)
+    np.testing.assert_allclose(qq3b.xip, qq.xip)
+    np.testing.assert_allclose(qq3b.xip_im, qq.xip_im)
+    np.testing.assert_allclose(qq3b.xim, qq.xim)
+    np.testing.assert_allclose(qq3b.xim_im, qq.xim_im)
+
     try:
         import fitsio
     except ImportError:
@@ -200,6 +216,16 @@ def test_direct():
         np.testing.assert_allclose(qq4.xim, qq.xim)
         np.testing.assert_allclose(qq4.xim_im, qq.xim_im)
 
+        qq4b = treecorr.QQCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(qq4b.npairs, qq.npairs)
+        np.testing.assert_allclose(qq4b.weight, qq.weight)
+        np.testing.assert_allclose(qq4b.meanr, qq.meanr)
+        np.testing.assert_allclose(qq4b.meanlogr, qq.meanlogr)
+        np.testing.assert_allclose(qq4b.xip, qq.xip)
+        np.testing.assert_allclose(qq4b.xip_im, qq.xip_im)
+        np.testing.assert_allclose(qq4b.xim, qq.xim)
+        np.testing.assert_allclose(qq4b.xim_im, qq.xim_im)
+
     with assert_raises(TypeError):
         qq2 += config
     qq4 = treecorr.QQCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -484,8 +510,7 @@ def test_qq():
     np.testing.assert_allclose(data['npairs'], qq.npairs)
 
     # Check the read function
-    qq2 = treecorr.QQCorrelation(bin_size=0.1, min_sep=10., max_sep=100., sep_units='arcmin')
-    qq2.read(out_file_name)
+    qq2 = treecorr.QQCorrelation.from_file(out_file_name)
     np.testing.assert_allclose(qq2.logr, qq.logr)
     np.testing.assert_allclose(qq2.meanr, qq.meanr)
     np.testing.assert_allclose(qq2.meanlogr, qq.meanlogr)
@@ -502,6 +527,7 @@ def test_qq():
     assert qq2.sep_units == qq.sep_units
     assert qq2.bin_type == qq.bin_type
 
+
 @timer
 def test_spherical():
     # This is the same field we used for test_qq, but put into spherical coords.
@@ -910,8 +936,7 @@ def make_spin4_field(rng):
     else:
         file_name = os.path.join('output','test_write_results_qq.fits')
         qq2.write(file_name, write_patch_results=True)
-        qq3 = treecorr.QQCorrelation(corr_params)
-        qq3.read(file_name)
+        qq3 = treecorr.QQCorrelation.from_file(file_name)
         cov3 = qq3.estimate_cov('jackknife')
         np.testing.assert_allclose(cov3, cov2)
 
@@ -999,8 +1024,7 @@ def test_twod():
     else:
         fits_name = 'output/qq_twod.fits'
         qq.write(fits_name)
-        qq2 = treecorr.QQCorrelation(bin_size=2, nbins=nbins, bin_type='TwoD')
-        qq2.read(fits_name)
+        qq2 = treecorr.QQCorrelation.from_file(fits_name)
         np.testing.assert_allclose(qq2.npairs, qq.npairs)
         np.testing.assert_allclose(qq2.weight, qq.weight)
         np.testing.assert_allclose(qq2.meanr, qq.meanr)
@@ -1012,8 +1036,7 @@ def test_twod():
 
     ascii_name = 'output/qq_twod.txt'
     qq.write(ascii_name, precision=16)
-    qq3 = treecorr.QQCorrelation(bin_size=2, nbins=nbins, bin_type='TwoD')
-    qq3.read(ascii_name)
+    qq3 = treecorr.QQCorrelation.from_file(ascii_name)
     np.testing.assert_allclose(qq3.npairs, qq.npairs)
     np.testing.assert_allclose(qq3.weight, qq.weight)
     np.testing.assert_allclose(qq3.meanr, qq.meanr)
diff --git a/tests/test_reader.py b/tests/test_reader.py
index c9b77903..a49e2bb6 100644
--- a/tests/test_reader.py
+++ b/tests/test_reader.py
@@ -34,15 +34,15 @@ def test_fits_reader():
 
     # Check things not allowed if not in context
     with assert_raises(RuntimeError):
-        r.read(['RA'], slice(0,10,2), 1)
+        r.read(['RA'], slice(0,10,2), ext=1)
     with assert_raises(RuntimeError):
         r.read('RA')
     with assert_raises(RuntimeError):
-        r.row_count('DEC', 1)
+        r.row_count('DEC', ext=1)
     with assert_raises(RuntimeError):
         r.row_count()
     with assert_raises(RuntimeError):
-        r.names(1)
+        r.names(ext=1)
     with assert_raises(RuntimeError):
         r.names()
     with assert_raises(RuntimeError):
@@ -65,15 +65,15 @@ def test_fits_reader():
 
         s = slice(0, 10, 2)
         for ext in [1, 'AARDWOLF']:
-            data = r.read(['RA'], s, ext)
-            dec = r.read('DEC', s, ext)
+            data = r.read(['RA'], s, ext=ext)
+            dec = r.read('DEC', s, ext=ext)
             assert data['RA'].size == 5
             assert dec.size == 5
 
-            assert r.row_count('RA', ext) == 390935
-            assert r.row_count('GAMMA1', ext) == 390935
-            assert set(r.names(ext)) == set("INDEX RA DEC Z EPSILON GAMMA1 GAMMA2 KAPPA MU".split())
-            assert set(r.names(ext)) == set(r.names())
+            assert r.row_count('RA', ext=ext) == 390935
+            assert r.row_count('GAMMA1', ext=ext) == 390935
+            assert set(r.names(ext=ext)) == set("INDEX RA DEC Z EPSILON GAMMA1 GAMMA2 KAPPA MU".split())
+            assert set(r.names(ext=ext)) == set(r.names())
 
         # Can read without slice or ext to use defaults
         assert r.row_count() == 390935
@@ -86,20 +86,20 @@ def test_fits_reader():
         mu = d['MU']
 
         # check we can also index by integer, not just number
-        d = r.read(['DEC'], np.arange(10), 'AARDWOLF')
+        d = r.read(['DEC'], np.arange(10), ext='AARDWOLF')
         assert d.size==10
 
     # Again check things not allowed if not in context
     with assert_raises(RuntimeError):
-        r.read(['RA'], slice(0,10,2), 1)
+        r.read(['RA'], slice(0,10,2), ext=1)
     with assert_raises(RuntimeError):
         r.read('RA')
     with assert_raises(RuntimeError):
-        r.row_count('DEC', 1)
+        r.row_count('DEC', ext=1)
     with assert_raises(RuntimeError):
         r.row_count()
     with assert_raises(RuntimeError):
-        r.names(1)
+        r.names(ext=1)
     with assert_raises(RuntimeError):
         r.names()
     with assert_raises(RuntimeError):
@@ -117,6 +117,17 @@ def test_fits_reader():
     assert np.array_equal(data['KAPPA'], kappa)
     assert np.array_equal(data['MU'], mu)
 
+    # Test write_array, read_array
+    cov = np.random.normal(5, 1, size=(10,10))
+    with FitsWriter(os.path.join('output','test_fits_writer_ar.fits')) as w:
+        w.write_array(cov)
+        w.write_array(2*cov, ext='double')
+    with FitsReader(os.path.join('output','test_fits_writer_ar.fits')) as r:
+        cov1 = r.read_array(cov.shape)
+        cov2 = r.read_array(cov.shape, ext='double')
+    np.testing.assert_array_equal(cov1, cov)
+    np.testing.assert_array_equal(cov2, 2*cov)
+
     # Use make_writer, make_reader
     with make_writer(os.path.join('output','test_fits_writer.fits')) as w:
         w.write(['KAPPA', 'MU'], [kappa, mu], params={'test': True})
@@ -179,15 +190,15 @@ def test_hdf_reader():
 
     # Check things not allowed if not in context
     with assert_raises(RuntimeError):
-        r.read(['RA'], slice(0,10,2), '/')
+        r.read(['RA'], slice(0,10,2), ext='/')
     with assert_raises(RuntimeError):
         r.read('RA')
     with assert_raises(RuntimeError):
-        r.row_count('DEC', '/')
+        r.row_count('DEC', ext='/')
     with assert_raises(RuntimeError):
         r.row_count('DEC')
     with assert_raises(RuntimeError):
-        r.names('/')
+        r.names(ext='/')
     with assert_raises(RuntimeError):
         r.names()
     with assert_raises(RuntimeError):
@@ -216,13 +227,13 @@ def test_hdf_reader():
         assert dec.size == 5
 
         assert r.row_count('RA') == 390935
-        assert r.row_count('RA','/') == 390935
+        assert r.row_count('RA', ext='/') == 390935
         assert r.row_count('GAMMA1') == 390935
         # Unlike the other readers, this needs a column name.
         with assert_raises(TypeError):
             r.row_count()
         assert set(r.names()) == set("INDEX RA DEC Z EPSILON GAMMA1 GAMMA2 KAPPA MU".split())
-        assert set(r.names('/')) == set(r.names())
+        assert set(r.names(ext='/')) == set(r.names())
 
         # Can read without slice or ext to use defaults
         g2 = r.read('GAMMA2')
@@ -235,15 +246,15 @@ def test_hdf_reader():
 
     # Again check things not allowed if not in context
     with assert_raises(RuntimeError):
-        r.read(['RA'], slice(0,10,2), '/')
+        r.read(['RA'], slice(0,10,2), ext='/')
     with assert_raises(RuntimeError):
         r.read('RA')
     with assert_raises(RuntimeError):
-        r.row_count('DEC', '/')
+        r.row_count('DEC', ext='/')
     with assert_raises(RuntimeError):
         r.row_count('DEC')
     with assert_raises(RuntimeError):
-        r.names('/')
+        r.names(ext='/')
     with assert_raises(RuntimeError):
         r.names()
     with assert_raises(RuntimeError):
@@ -261,6 +272,17 @@ def test_hdf_reader():
     assert np.array_equal(data['KAPPA'], kappa)
     assert np.array_equal(data['MU'], mu)
 
+    # Test write_array, read_array
+    cov = np.random.normal(5, 1, size=(10,10))
+    with HdfWriter(os.path.join('output','test_fits_writer_ar.hdf')) as w:
+        w.write_array(cov)
+        w.write_array(2*cov, ext='double')
+    with HdfReader(os.path.join('output','test_fits_writer_ar.hdf')) as r:
+        cov1 = r.read_array(cov.shape)
+        cov2 = r.read_array(cov.shape, ext='double')
+    np.testing.assert_array_equal(cov1, cov)
+    np.testing.assert_array_equal(cov2, 2*cov)
+
     # Not allowed to write when not in with context
     w = HdfWriter(os.path.join('output','test_hdf_writer.hdf'))
     with assert_raises(RuntimeError):
@@ -281,17 +303,17 @@ def test_parquet_reader():
 
     # Check things not allowed if not in context
     with assert_raises(RuntimeError):
-        r.read(['RA'], slice(0,10,2), None)
+        r.read(['RA'], slice(0,10,2), ext=None)
     with assert_raises(RuntimeError):
         r.read('RA')
     with assert_raises(RuntimeError):
-        r.row_count('DEC', None)
+        r.row_count('DEC', ext=None)
     with assert_raises(RuntimeError):
         r.row_count('DEC')
     with assert_raises(RuntimeError):
         r.row_count()
     with assert_raises(RuntimeError):
-        r.names(None)
+        r.names(ext=None)
     with assert_raises(RuntimeError):
         r.names()
 
@@ -317,27 +339,27 @@ def test_parquet_reader():
         assert dec.size == 5
 
         assert r.row_count('RA') == 390935
-        assert r.row_count('RA',None) == 390935
+        assert r.row_count('RA', ext=None) == 390935
         assert r.row_count('GAMMA1') == 390935
         assert r.row_count() == 390935
         print('names = ',set(r.names()))
         print('names = ',set("INDEX RA DEC Z GAMMA1 GAMMA2 KAPPA MU".split()))
         assert set(r.names()) == set("INDEX RA DEC Z GAMMA1 GAMMA2 KAPPA MU".split())
-        assert set(r.names(None)) == set(r.names())
+        assert set(r.names(ext=None)) == set(r.names())
 
     # Again check things not allowed if not in context
     with assert_raises(RuntimeError):
-        r.read(['RA'], slice(0,10,2), None)
+        r.read(['RA'], slice(0,10,2), ext=None)
     with assert_raises(RuntimeError):
         r.read('RA')
     with assert_raises(RuntimeError):
-        r.row_count('DEC', None)
+        r.row_count('DEC', ext=None)
     with assert_raises(RuntimeError):
         r.row_count('DEC')
     with assert_raises(RuntimeError):
         r.row_count()
     with assert_raises(RuntimeError):
-        r.names(None)
+        r.names(ext=None)
     with assert_raises(RuntimeError):
         r.names()
 
@@ -352,11 +374,11 @@ def _test_ascii_reader(r, has_names=True):
     with assert_raises(RuntimeError):
         r.read('ra')
     with assert_raises(RuntimeError):
-        r.row_count(1, None)
+        r.row_count(1, ext=None)
     with assert_raises(RuntimeError):
         r.row_count()
     with assert_raises(RuntimeError):
-        r.names(None)
+        r.names(ext=None)
     with assert_raises(RuntimeError):
         r.names()
 
@@ -388,7 +410,7 @@ def _test_ascii_reader(r, has_names=True):
         assert data[3][4] == 0.01816738  # x, row 9
         assert data[9][3] == 0.79008204  # z, row 7
 
-        assert r.row_count(1, None) == 20
+        assert r.row_count(1, ext=None) == 20
         assert r.row_count() == 20
         assert r.ncols == 12
         for i in range(12):
@@ -428,7 +450,7 @@ def _test_ascii_reader(r, has_names=True):
         assert data['x'][4] == 0.01816738
         assert data['z'][3] == 0.79008204
 
-        assert r.row_count('ra', None) == 20
+        assert r.row_count('ra', ext=None) == 20
         assert r.row_count() == 20
         assert r.ncols == 12
         names = ['ra', 'dec', 'x', 'y', 'k', 'g1', 'g2', 'w', 'z', 'r', 'wpos', 'flag']
@@ -464,11 +486,11 @@ def _test_ascii_reader(r, has_names=True):
         r.read('ra')
     r.nrows = None
     with assert_raises(RuntimeError):
-        r.row_count(1, None)
+        r.row_count(1, ext=None)
     with assert_raises(RuntimeError):
         r.row_count()
     with assert_raises(RuntimeError):
-        r.names(None)
+        r.names(ext=None)
     with assert_raises(RuntimeError):
         r.names()
 
@@ -485,6 +507,17 @@ def _test_ascii_reader(r, has_names=True):
         with assert_raises(OSError):
             params = r.read_params(ext='gg')
 
+    # Test write_array, read_array
+    cov = np.random.normal(5, 1, size=(10,10))
+    with AsciiWriter(os.path.join('output','test_fits_writer_ar.dat'), precision=16) as w:
+        w.write_array(cov)
+        w.write_array(2*cov, ext='double')
+    with AsciiReader(os.path.join('output','test_fits_writer_ar.dat')) as r:
+        cov1 = r.read_array(cov.shape)
+        cov2 = r.read_array(cov.shape, ext='double')
+    np.testing.assert_array_equal(cov1, cov)
+    np.testing.assert_array_equal(cov2, 2*cov)
+
     # Test no ext name
     with AsciiWriter(os.path.join('output','test_ascii_writer.dat'), precision=16) as w:
         w.write(['g1', 'g2'], [g1, g2], params={'test': True})
diff --git a/tests/test_tt.py b/tests/test_tt.py
index 69498d2a..810d6349 100644
--- a/tests/test_tt.py
+++ b/tests/test_tt.py
@@ -170,7 +170,7 @@ def test_direct():
 
     ascii_name = 'output/tt_ascii.txt'
     tt.write(ascii_name, precision=16)
-    tt3 = treecorr.TTCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    tt3 = treecorr.TTCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     tt3.read(ascii_name)
     np.testing.assert_allclose(tt3.npairs, tt.npairs)
     np.testing.assert_allclose(tt3.weight, tt.weight)
@@ -181,6 +181,22 @@ def test_direct():
     np.testing.assert_allclose(tt3.xim, tt.xim)
     np.testing.assert_allclose(tt3.xim_im, tt.xim_im)
 
+    # Check that the repr is minimal
+    assert repr(tt3) == f'TTCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # Simpler API using from_file:
+    with CaptureLog() as cl:
+        tt3b = treecorr.TTCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(tt3b.npairs, tt.npairs)
+    np.testing.assert_allclose(tt3b.weight, tt.weight)
+    np.testing.assert_allclose(tt3b.meanr, tt.meanr)
+    np.testing.assert_allclose(tt3b.meanlogr, tt.meanlogr)
+    np.testing.assert_allclose(tt3b.xip, tt.xip)
+    np.testing.assert_allclose(tt3b.xip_im, tt.xip_im)
+    np.testing.assert_allclose(tt3b.xim, tt.xim)
+    np.testing.assert_allclose(tt3b.xim_im, tt.xim_im)
+
     try:
         import fitsio
     except ImportError:
@@ -199,6 +215,16 @@ def test_direct():
         np.testing.assert_allclose(tt4.xim, tt.xim)
         np.testing.assert_allclose(tt4.xim_im, tt.xim_im)
 
+        tt4b = treecorr.TTCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(tt4b.npairs, tt.npairs)
+        np.testing.assert_allclose(tt4b.weight, tt.weight)
+        np.testing.assert_allclose(tt4b.meanr, tt.meanr)
+        np.testing.assert_allclose(tt4b.meanlogr, tt.meanlogr)
+        np.testing.assert_allclose(tt4b.xip, tt.xip)
+        np.testing.assert_allclose(tt4b.xip_im, tt.xip_im)
+        np.testing.assert_allclose(tt4b.xim, tt.xim)
+        np.testing.assert_allclose(tt4b.xim_im, tt.xim_im)
+
     with assert_raises(TypeError):
         tt2 += config
     tt4 = treecorr.TTCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -484,8 +510,7 @@ def test_tt():
     np.testing.assert_allclose(data['npairs'], tt.npairs)
 
     # Check the read function
-    tt2 = treecorr.TTCorrelation(bin_size=0.1, min_sep=10., max_sep=100., sep_units='arcmin')
-    tt2.read(out_file_name)
+    tt2 = treecorr.TTCorrelation.from_file(out_file_name)
     np.testing.assert_allclose(tt2.logr, tt.logr)
     np.testing.assert_allclose(tt2.meanr, tt.meanr)
     np.testing.assert_allclose(tt2.meanlogr, tt.meanlogr)
@@ -502,6 +527,7 @@ def test_tt():
     assert tt2.sep_units == tt.sep_units
     assert tt2.bin_type == tt.bin_type
 
+
 @timer
 def test_spherical():
     # This is the same field we used for test_tt, but put into spherical coords.
@@ -908,8 +934,7 @@ def make_spin3_field(rng):
     else:
         file_name = os.path.join('output','test_write_results_tt.fits')
         tt2.write(file_name, write_patch_results=True)
-        tt3 = treecorr.TTCorrelation(corr_params)
-        tt3.read(file_name)
+        tt3 = treecorr.TTCorrelation.from_file(file_name)
         cov3 = tt3.estimate_cov('jackknife')
         np.testing.assert_allclose(cov3, cov2)
 
@@ -997,8 +1022,7 @@ def test_twod():
     else:
         fits_name = 'output/tt_twod.fits'
         tt.write(fits_name)
-        tt2 = treecorr.TTCorrelation(bin_size=2, nbins=nbins, bin_type='TwoD')
-        tt2.read(fits_name)
+        tt2 = treecorr.TTCorrelation.from_file(fits_name)
         np.testing.assert_allclose(tt2.npairs, tt.npairs)
         np.testing.assert_allclose(tt2.weight, tt.weight)
         np.testing.assert_allclose(tt2.meanr, tt.meanr)
@@ -1010,8 +1034,7 @@ def test_twod():
 
     ascii_name = 'output/tt_twod.txt'
     tt.write(ascii_name, precision=16)
-    tt3 = treecorr.TTCorrelation(bin_size=2, nbins=nbins, bin_type='TwoD')
-    tt3.read(ascii_name)
+    tt3 = treecorr.TTCorrelation.from_file(ascii_name)
     np.testing.assert_allclose(tt3.npairs, tt.npairs)
     np.testing.assert_allclose(tt3.weight, tt.weight)
     np.testing.assert_allclose(tt3.meanr, tt.meanr)
diff --git a/tests/test_twod.py b/tests/test_twod.py
index 9d15f738..469a0b14 100644
--- a/tests/test_twod.py
+++ b/tests/test_twod.py
@@ -227,8 +227,7 @@ def test_twod():
     else:
         fits_name = 'output/gg_twod.fits'
         gg.write(fits_name)
-        gg2 = treecorr.GGCorrelation(bin_size=2, nbins=nbins, bin_type='TwoD')
-        gg2.read(fits_name)
+        gg2 = treecorr.GGCorrelation.from_file(fits_name)
         np.testing.assert_allclose(gg2.npairs, gg.npairs)
         np.testing.assert_allclose(gg2.weight, gg.weight)
         np.testing.assert_allclose(gg2.meanr, gg.meanr)
@@ -240,8 +239,7 @@ def test_twod():
 
     ascii_name = 'output/gg_twod.txt'
     gg.write(ascii_name, precision=16)
-    gg3 = treecorr.GGCorrelation(bin_size=2, nbins=nbins, bin_type='TwoD')
-    gg3.read(ascii_name)
+    gg3 = treecorr.GGCorrelation.from_file(ascii_name)
     np.testing.assert_allclose(gg3.npairs, gg.npairs)
     np.testing.assert_allclose(gg3.weight, gg.weight)
     np.testing.assert_allclose(gg3.meanr, gg.meanr)
diff --git a/tests/test_vv.py b/tests/test_vv.py
index 5ffcae56..8e3214e0 100644
--- a/tests/test_vv.py
+++ b/tests/test_vv.py
@@ -170,7 +170,7 @@ def test_direct():
 
     ascii_name = 'output/vv_ascii.txt'
     vv.write(ascii_name, precision=16)
-    vv3 = treecorr.VVCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins)
+    vv3 = treecorr.VVCorrelation(min_sep=min_sep, max_sep=max_sep, nbins=nbins, bin_type='Log')
     vv3.read(ascii_name)
     np.testing.assert_allclose(vv3.npairs, vv.npairs)
     np.testing.assert_allclose(vv3.weight, vv.weight)
@@ -181,6 +181,22 @@ def test_direct():
     np.testing.assert_allclose(vv3.xim, vv.xim)
     np.testing.assert_allclose(vv3.xim_im, vv.xim_im)
 
+    # Check that the repr is minimal
+    assert repr(vv3) == f'VVCorrelation(min_sep={min_sep}, max_sep={max_sep}, nbins={nbins})'
+
+    # Simpler API using from_file:
+    with CaptureLog() as cl:
+        vv3b = treecorr.VVCorrelation.from_file(ascii_name, logger=cl.logger)
+    assert ascii_name in cl.output
+    np.testing.assert_allclose(vv3b.npairs, vv.npairs)
+    np.testing.assert_allclose(vv3b.weight, vv.weight)
+    np.testing.assert_allclose(vv3b.meanr, vv.meanr)
+    np.testing.assert_allclose(vv3b.meanlogr, vv.meanlogr)
+    np.testing.assert_allclose(vv3b.xip, vv.xip)
+    np.testing.assert_allclose(vv3b.xip_im, vv.xip_im)
+    np.testing.assert_allclose(vv3b.xim, vv.xim)
+    np.testing.assert_allclose(vv3b.xim_im, vv.xim_im)
+
     try:
         import fitsio
     except ImportError:
@@ -199,6 +215,16 @@ def test_direct():
         np.testing.assert_allclose(vv4.xim, vv.xim)
         np.testing.assert_allclose(vv4.xim_im, vv.xim_im)
 
+        vv4b = treecorr.VVCorrelation.from_file(fits_name)
+        np.testing.assert_allclose(vv4b.npairs, vv.npairs)
+        np.testing.assert_allclose(vv4b.weight, vv.weight)
+        np.testing.assert_allclose(vv4b.meanr, vv.meanr)
+        np.testing.assert_allclose(vv4b.meanlogr, vv.meanlogr)
+        np.testing.assert_allclose(vv4b.xip, vv.xip)
+        np.testing.assert_allclose(vv4b.xip_im, vv.xip_im)
+        np.testing.assert_allclose(vv4b.xim, vv.xim)
+        np.testing.assert_allclose(vv4b.xim_im, vv.xim_im)
+
     with assert_raises(TypeError):
         vv2 += config
     vv4 = treecorr.VVCorrelation(min_sep=min_sep/2, max_sep=max_sep, nbins=nbins)
@@ -486,8 +512,7 @@ def test_vv():
     np.testing.assert_allclose(data['npairs'], vv.npairs)
 
     # Check the read function
-    vv2 = treecorr.VVCorrelation(bin_size=0.1, min_sep=1., max_sep=100., sep_units='arcmin')
-    vv2.read(out_file_name)
+    vv2 = treecorr.VVCorrelation.from_file(out_file_name)
     np.testing.assert_allclose(vv2.logr, vv.logr)
     np.testing.assert_allclose(vv2.meanr, vv.meanr)
     np.testing.assert_allclose(vv2.meanlogr, vv.meanlogr)
@@ -504,6 +529,7 @@ def test_vv():
     assert vv2.sep_units == vv.sep_units
     assert vv2.bin_type == vv.bin_type
 
+
 @timer
 def test_spherical():
     # This is the same field we used for test_vv, but put into spherical coords.
@@ -916,8 +942,7 @@ def make_velocity_field(rng):
     else:
         file_name = os.path.join('output','test_write_results_vv.fits')
         vv2.write(file_name, write_patch_results=True)
-        vv3 = treecorr.VVCorrelation(corr_params)
-        vv3.read(file_name)
+        vv3 = treecorr.VVCorrelation.from_file(file_name)
         cov3 = vv3.estimate_cov('jackknife')
         np.testing.assert_allclose(cov3, cov2)
 
@@ -1005,8 +1030,7 @@ def test_twod():
     else:
         fits_name = 'output/vv_twod.fits'
         vv.write(fits_name)
-        vv2 = treecorr.VVCorrelation(bin_size=2, nbins=nbins, bin_type='TwoD')
-        vv2.read(fits_name)
+        vv2 = treecorr.VVCorrelation.from_file(fits_name)
         np.testing.assert_allclose(vv2.npairs, vv.npairs)
         np.testing.assert_allclose(vv2.weight, vv.weight)
         np.testing.assert_allclose(vv2.meanr, vv.meanr)
@@ -1018,8 +1042,7 @@ def test_twod():
 
     ascii_name = 'output/vv_twod.txt'
     vv.write(ascii_name, precision=16)
-    vv3 = treecorr.VVCorrelation(bin_size=2, nbins=nbins, bin_type='TwoD')
-    vv3.read(ascii_name)
+    vv3 = treecorr.VVCorrelation.from_file(ascii_name)
     np.testing.assert_allclose(vv3.npairs, vv.npairs)
     np.testing.assert_allclose(vv3.weight, vv.weight)
     np.testing.assert_allclose(vv3.meanr, vv.meanr)