ksunden: Also cleans up a test which was closing files improperly, leading to them not being removed from tmp
ksunden: oops, commit message missed the w and said qt instead of wt, oh well

diff --git a/WrightTools/__init__.py b/WrightTools/__init__.py
index e561ca0..667c127 100644
--- a/WrightTools/__init__.py
+++ b/WrightTools/__init__.py
@@ -19,6 +19,7 @@ from . import units
 from . import exceptions
 
 from ._open import *
+from ._close import *
 from .collection._collection import *
 from .data._data import *
 
diff --git a/WrightTools/_close.py b/WrightTools/_close.py
new file mode 100644
index 0000000..87ac5dc
--- /dev/null
+++ b/WrightTools/_close.py
@@ -0,0 +1,27 @@
+"""Function to close all open wt5 files."""
+
+
+# --- import -------------------------------------------------------------------------------------
+
+
+from . import _group as wt_group
+
+
+# --- define -------------------------------------------------------------------------------------
+
+
+__all__ = ["close"]
+
+
+# --- functions ----------------------------------------------------------------------------------
+
+
+def close():
+    """Close all open wt5 files.
+    
+    Warning
+    -------
+    This will make any open objects unusable and delete unsaved temporary files.
+    """
+    while len(wt_group.Group._instances) > 0:
+        wt_group.Group._instances.popitem()[1].close()
diff --git a/WrightTools/artists/_quick.py b/WrightTools/artists/_quick.py
index ffc6982..47078ac 100644
--- a/WrightTools/artists/_quick.py
+++ b/WrightTools/artists/_quick.py
@@ -156,6 +156,7 @@ def quick1D(
             if verbose:
                 print("image saved at", fpath)
             out.append(fpath)
+    chopped.close()
     return out
 
 
@@ -372,4 +373,5 @@ def quick2D(
             if verbose:
                 print("image saved at", fpath)
             out.append(fpath)
+    chopped.close()
     return out
diff --git a/WrightTools/data/_channel.py b/WrightTools/data/_channel.py
index 91a04ed..03d4973 100644
--- a/WrightTools/data/_channel.py
+++ b/WrightTools/data/_channel.py
@@ -152,14 +152,17 @@ class Channel(Dataset):
 
         factor : number (optional)
             Tolerance factor.  Default is 3.
-        replace : {'nan', 'mean', number} (optional)
+        replace : {'nan', 'mean', 'exclusive_mean', number} (optional)
             Behavior of outlier replacement. Default is nan.
 
             nan
                 Outliers are replaced by numpy nans.
 
             mean
-                Outliers are replaced by the mean of its neighborhood.
+                Outliers are replaced by the mean of its neighborhood, including itself.
+
+            exclusive_mean
+                Outilers are replaced by the mean of its neighborhood, not including itself.
 
             number
                 Array becomes given number.
@@ -177,6 +180,7 @@ class Channel(Dataset):
         warnings.warn("trim", category=wt_exceptions.EntireDatasetInMemoryWarning)
         outliers = []
         means = []
+        ex_means = []
         # find outliers
         for idx in np.ndindex(self.shape):
             slices = []
@@ -186,26 +190,33 @@ class Channel(Dataset):
                 slices.append(slice(start, stop, 1))
             neighbors = self[slices]
             mean = np.nanmean(neighbors)
+            sum_ = np.nansum(neighbors)
             limit = np.nanstd(neighbors) * factor
             if np.abs(self[idx] - mean) > limit:
                 outliers.append(idx)
                 means.append(mean)
+                # Note, "- 1" is to exclude the point itself, which is not nan, in order
+                # to enter this if block, as `np.abs(nan - mean)` is nan, which would
+                # evaluate to False
+                ex_means.append((sum_ - self[idx]) / (np.sum(~np.isnan(neighbors)) - 1))
+
         # replace outliers
         i = tuple(zip(*outliers))
-        if replace == "nan":
-            arr = self[:]
-            arr[i] = np.nan
-            self[:] = arr
-        elif replace == "mean":
-            arr = self[:]
-            arr[i] = means
-            self[:] = arr
-        elif isinstance(replace, numbers.Number):
-            arr = self[:]
-            arr[i] = replace
-            self[:] = arr
-        else:
-            raise KeyError("replace must be one of {nan, mean} or some number")
+
+        if len(i) == 0:
+            if verbose:
+                print("No outliers found")
+            return []
+
+        replace = {"nan": np.nan, "mean": means, "exclusive_mean": ex_means}.get(replace, replace)
+
+        # This may someday be available in h5py directly, but seems that day is not yet.
+        # This is annoying because it is the only reason we hold the whole set in memory.
+        # KFS 2019-03-21
+        arr = self[:]
+        arr[i] = replace
+        self[:] = arr
+
         # finish
         if verbose:
             print("%i outliers removed" % len(outliers))

`wt.close` method to close all open WrightTools objects
diff --git a/tests/base.py b/tests/base.py
index e471faf..c58119a 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -5,6 +5,7 @@
 
 
 import os
+import pytest
 
 import WrightTools as wt
 from WrightTools import datasets
@@ -74,7 +75,8 @@ def test_nested():
     c = wt.Collection()
     cc = c.create_collection()
     assert c.fid.id == cc.fid.id
-    c.file.close()
+    c.close()
+    assert not os.path.isfile(c.filepath)
     assert c.id.valid == 0
     assert cc.id.valid == 0
 
@@ -87,6 +89,15 @@ def test_open_context():
     assert d.id.valid == 0
 
 
+def test_close():
+    d = wt.Data()
+    path = d.filepath
+    wt.close()
+    assert not os.path.isfile(path)
+    with pytest.raises(ValueError):
+        d.file
+
+
 if __name__ == "__main__":
     test_named_root_collection()
     test_named_root_data()
@@ -96,3 +107,4 @@ if __name__ == "__main__":
     test_tempfile_cleanup()
     test_nested()
     test_open_context()
+    test_close()
diff --git a/tests/data/trim.py b/tests/data/trim.py
index 2a94167..f096904 100644
--- a/tests/data/trim.py
+++ b/tests/data/trim.py
@@ -32,18 +32,23 @@ def test_trim_2Dgauss():
     d.create_channel("damaged1", arr2)
     d.create_channel("damaged2", arr2)
     d.create_channel("damaged3", arr2)
+    d.create_channel("damaged4", arr2)
     d.transform("x", "y")
     # trim
+    d.original.trim([2, 2], factor=2)
     d.damaged1.trim([2, 2], factor=2)
     d.damaged2.trim([2, 2], factor=2, replace="mean")
     d.damaged3.trim([2, 2], factor=2, replace=0.5)
+    d.damaged4.trim([2, 2], factor=2, replace="exclusive_mean")
     # now heal
     d.create_channel("healed_linear", d.damaged1[:])
     d.heal(channel="healed_linear", fill_value=0, method="linear")
     # check
-    assert np.allclose(d.original[:], d.healed_linear[:], rtol=1e-1, atol=1e-1)
-    assert np.allclose(d.original[:], d.damaged2[:], rtol=1e-1, atol=9e-1)
-    assert np.allclose(d.original[:], d.damaged3[:], rtol=1e-1, atol=5e-1)
+    np.testing.assert_allclose(d.original[:], d.original[:], rtol=1e-1, atol=1e-1)
+    np.testing.assert_allclose(d.original[:], d.healed_linear[:], rtol=1e-1, atol=1e-1)
+    np.testing.assert_allclose(d.original[:], d.damaged2[:], rtol=1e-1, atol=9e-1)
+    np.testing.assert_allclose(d.original[:], d.damaged3[:], rtol=1e-1, atol=5e-1)
+    np.testing.assert_allclose(d.original[:], d.damaged4[:], rtol=1e-1, atol=3e-1)
 
 
 def test_trim_3Dgauss():
@@ -67,7 +72,7 @@ def test_trim_3Dgauss():
     # trim
     d.damaged.trim([2, 2, 2], factor=2, replace="mean")
     # check
-    assert np.allclose(d.original[:], d.damaged[:], rtol=1e-1, atol=9e-1)
+    np.testing.assert_allclose(d.original[:], d.damaged[:], rtol=1e-1, atol=9e-1)
 
 
 if __name__ == "__main__":

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
apt-get update apt-get install -y gcc pip install cython
3.6
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
black==22.8.0
cached-property==1.5.2
certifi==2021.5.30
cfgv==3.3.1
click==8.0.4
coverage==6.2
cycler==0.11.0
Cython==3.0.12
dataclasses==0.8
distlib==0.3.9
execnet==1.9.0
filelock==3.4.1
h5py==3.1.0
identify==2.4.4
imageio==2.15.0
importlib-metadata==4.8.3
importlib-resources==5.2.3
iniconfig==1.1.1
kiwisolver==1.3.1
matplotlib==3.3.4
mypy-extensions==1.0.0
nodeenv==1.6.0
numexpr==2.8.1
numpy==1.19.5
packaging==21.3
pathspec==0.9.0
Pillow==8.4.0
platformdirs==2.4.0
pluggy==1.0.0
pre-commit==2.17.0
py==1.11.0
pydocstyle==6.3.0
pyparsing==3.1.4
pytest==7.0.1
pytest-asyncio==0.16.0
pytest-cov==4.0.0
pytest-mock==3.6.1
pytest-xdist==3.0.2
python-dateutil==2.9.0.post0
PyYAML==6.0.1
scipy==1.5.4
six==1.17.0
snowballstemmer==2.2.0
swebench-matterhorn @ file:///swebench_matterhorn
tidy_headers==1.0.3
toml==0.10.2
tomli==1.2.3
typed-ast==1.5.5
typing_extensions==4.1.1
virtualenv==20.16.2
-e git+https://github.com/wright-group/WrightTools.git@dc02147913c603792e8a7c12228dc292334d8084#egg=WrightTools
zipp==3.6.0

name: WrightTools
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - black==22.8.0
      - cached-property==1.5.2
      - cfgv==3.3.1
      - click==8.0.4
      - coverage==6.2
      - cycler==0.11.0
      - cython==3.0.12
      - dataclasses==0.8
      - distlib==0.3.9
      - execnet==1.9.0
      - filelock==3.4.1
      - h5py==3.1.0
      - identify==2.4.4
      - imageio==2.15.0
      - importlib-metadata==4.8.3
      - importlib-resources==5.2.3
      - iniconfig==1.1.1
      - kiwisolver==1.3.1
      - matplotlib==3.3.4
      - mypy-extensions==1.0.0
      - nodeenv==1.6.0
      - numexpr==2.8.1
      - numpy==1.19.5
      - packaging==21.3
      - pathspec==0.9.0
      - pillow==8.4.0
      - platformdirs==2.4.0
      - pluggy==1.0.0
      - pre-commit==2.17.0
      - py==1.11.0
      - pydocstyle==6.3.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytest-asyncio==0.16.0
      - pytest-cov==4.0.0
      - pytest-mock==3.6.1
      - pytest-xdist==3.0.2
      - python-dateutil==2.9.0.post0
      - pyyaml==6.0.1
      - scipy==1.5.4
      - six==1.17.0
      - snowballstemmer==2.2.0
      - swebench-matterhorn==0.0.0
      - tidy-headers==1.0.3
      - toml==0.10.2
      - tomli==1.2.3
      - typed-ast==1.5.5
      - typing-extensions==4.1.1
      - virtualenv==20.16.2
      - zipp==3.6.0
prefix: /opt/conda/envs/WrightTools

tests/base.py::test_close tests/data/trim.py::test_trim_2Dgauss
tests/base.py::test_open_context
tests/base.py::test_named_root_collection tests/base.py::test_named_root_data tests/base.py::test_parent_child tests/base.py::test_single_instance_collection tests/base.py::test_single_instance_data tests/base.py::test_tempfile_cleanup tests/base.py::test_nested tests/data/trim.py::test_trim_3Dgauss
WrightTools/artists/_quick.py WrightTools/__init__.py WrightTools/_close.py WrightTools/data/_channel.py
WrightTools/artists/_quick.py WrightTools/__init__.py WrightTools/_close.py WrightTools/data/_channel.py