Repository: Project-MONAI/MONAI

Issue:
PersistentDataset breaks if given existing folder
**Describe the bug**
If we pass a string to the cache_dir argument of the PersistentDataset that points towards an already existing directory the dataset creation will error out. 

**To Reproduce**
Create a folder (I used pathlib.Path.mkdir) and pass the absolute path string to cache_dir argument.

**Expected behavior**
The existing folder is used and caching happens.

**Actual behavior**
The following error happens:
```
Traceback (most recent call last):
  File "/home/danieltudosiu/projects/nmpevqvae/code_fine_tuning/experiment.py", line 123, in <module>
    conditional_in_channels = iter(data_loader).__next__()[
  File "/home/danieltudosiu/projects/nmpevqvae/evenv/lib/python3.6/site-packages/torch/utils/data/dataloader.py", line 363, in __next__
    data = self._next_data()
  File "/home/danieltudosiu/projects/nmpevqvae/evenv/lib/python3.6/site-packages/torch/utils/data/dataloader.py", line 403, in _next_data
    data = self._dataset_fetcher.fetch(index)  # may raise StopIteration
  File "/home/danieltudosiu/projects/nmpevqvae/evenv/lib/python3.6/site-packages/torch/utils/data/_utils/fetch.py", line 44, in fetch
    data = [self.dataset[idx] for idx in possibly_batched_index]
  File "/home/danieltudosiu/projects/nmpevqvae/evenv/lib/python3.6/site-packages/torch/utils/data/_utils/fetch.py", line 44, in <listcomp>
    data = [self.dataset[idx] for idx in possibly_batched_index]
  File "/home/danieltudosiu/projects/nmpevqvae/evenv/lib/python3.6/site-packages/monai/data/dataset.py", line 197, in __getitem__
    pre_random_item = self._pre_first_random_cachecheck(self.data[index])
  File "/home/danieltudosiu/projects/nmpevqvae/evenv/lib/python3.6/site-packages/monai/data/dataset.py", line 176, in _pre_first_random_cachecheck
    json.dumps(item_transformed, sort_keys=True).encode("utf-8")
  File "/usr/lib/python3.6/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "/usr/lib/python3.6/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python3.6/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/usr/lib/python3.6/json/encoder.py", line 180, in default
    o.__class__.__name__)
TypeError: Object of type 'ndarray' is not JSON serializable
```

**Environment (please complete the following information):**
 - OS - Ubuntu 18.04
 - Python version - 3.6
 - MONAI version [e.g. git commit hash] - 0.2 release
 - CUDA/cuDNN version - 11/8.0.2
 - GPU models and configuration - 2 x Titan V



Discussion:

Resolving patch:
diff --git a/monai/data/dataset.py b/monai/data/dataset.py
--- a/monai/data/dataset.py
+++ b/monai/data/dataset.py
@@ -92,6 +92,10 @@ class PersistentDataset(Dataset):
 
     Subsequent uses of a dataset directly read pre-processed results from `cache_dir`
     followed by applying the random dependant parts of transform processing.
+
+    Note:
+        The input data must be a list of file paths and will hash them as cache keys.
+
     """
 
     def __init__(
@@ -108,13 +112,18 @@ def __init__(
             cache_dir: If specified, this is the location for persistent storage
                 of pre-computed transformed data tensors. The cache_dir is computed once, and
                 persists on disk until explicitly removed.  Different runs, programs, experiments
-                may share a common cache dir provided that the transforms pre-processing is
-                consistent.
+                may share a common cache dir provided that the transforms pre-processing is consistent.
+                If the cache_dir doesn't exist, will automatically create it.
         """
         if not isinstance(transform, Compose):
             transform = Compose(transform)
         super().__init__(data=data, transform=transform)
         self.cache_dir = Path(cache_dir) if cache_dir is not None else None
+        if self.cache_dir is not None:
+            if not self.cache_dir.exists():
+                self.cache_dir.mkdir(parents=True)
+            if not self.cache_dir.is_dir():
+                raise ValueError("cache_dir must be a directory.")
 
     def _pre_first_random_transform(self, item_transformed):
         """
@@ -177,13 +186,9 @@ def _pre_first_random_cachecheck(self, item_transformed):
         if item_transformed.get("cached", False) is False:
             hashfile = None
             if self.cache_dir is not None:
-                cache_dir_path = Path(self.cache_dir)
-                if cache_dir_path.is_dir():
-                    # TODO: Find way to hash transforms content as part of the cache
-                    data_item_md5 = hashlib.md5(
-                        json.dumps(item_transformed, sort_keys=True).encode("utf-8")
-                    ).hexdigest()
-                    hashfile = Path(cache_dir_path) / f"{data_item_md5}.pt"
+                # TODO: Find way to hash transforms content as part of the cache
+                data_item_md5 = hashlib.md5(json.dumps(item_transformed, sort_keys=True).encode("utf-8")).hexdigest()
+                hashfile = self.cache_dir / f"{data_item_md5}.pt"
 
             if hashfile is not None and hashfile.is_file():
                 item_transformed = torch.load(hashfile)
@@ -191,7 +196,7 @@ def _pre_first_random_cachecheck(self, item_transformed):
                 item_transformed = self._pre_first_random_transform(item_transformed)
                 if hashfile is not None:
                     # add sentinel flag to indicate that the transforms have already been computed.
-                    item_transformed["cache"] = True
+                    item_transformed["cached"] = True
                     # NOTE: Writing to ".temp_write_cache" and then using a nearly atomic rename operation
                     #       to make the cache more robust to manual killing of parent process
                     #       which may leave partially written cache files in an incomplete state
@@ -378,7 +383,7 @@ def __init__(
         num_replace_workers: int = 0,
     ) -> None:
         super().__init__(data, transform, cache_num, cache_rate, num_init_workers)
-        if self.cache_num == len(data):
+        if self.cache_num >= len(data):
             raise ValueError("cache_num must be smaller than dataset length to support replacement.")
         if replace_rate <= 0:
             raise ValueError("replace_rate must be greater than 0, otherwise, please use CacheDataset.")
@@ -422,7 +427,7 @@ def start(self):
         Start the background thread to replace training items for every epoch.
 
         """
-        if self._replace_mgr is None or not self._is_started():
+        if self._replace_mgr is None or not self.is_started():
             self._restart()
 
     def _restart(self):


Test patch:
diff --git a/tests/test_persistentdataset.py b/tests/test_persistentdataset.py
--- a/tests/test_persistentdataset.py
+++ b/tests/test_persistentdataset.py
@@ -65,11 +65,12 @@ def test_shape(self, transform, expected_shape):
                 },
             ]
 
-            dataset_precached = PersistentDataset(data=test_data, transform=transform, cache_dir=tempdir)
+            cache_dir = os.path.join(os.path.join(tempdir, "cache"), "data")
+            dataset_precached = PersistentDataset(data=test_data, transform=transform, cache_dir=cache_dir)
             data1_precached = dataset_precached[0]
             data2_precached = dataset_precached[1]
 
-            dataset_postcached = PersistentDataset(data=test_data, transform=transform, cache_dir=tempdir)
+            dataset_postcached = PersistentDataset(data=test_data, transform=transform, cache_dir=cache_dir)
             data1_postcached = dataset_postcached[0]
             data2_postcached = dataset_postcached[1]
 
diff --git a/tests/test_smartcachedataset.py b/tests/test_smartcachedataset.py
--- a/tests/test_smartcachedataset.py
+++ b/tests/test_smartcachedataset.py
@@ -57,15 +57,16 @@ def test_shape(self, replace_rate, num_replace_workers, transform):
             for i in range(dataset.cache_num):
                 self.assertIsNotNone(dataset._cache[i])
 
-            dataset.start()
-            for _ in range(3):
-                dataset.update_cache()
-                self.assertIsNotNone(dataset._cache[15])
-                if isinstance(dataset._cache[15]["image"], np.ndarray):
-                    np.testing.assert_allclose(dataset._cache[15]["image"], dataset._cache[15]["label"])
-                else:
-                    self.assertIsInstance(dataset._cache[15]["image"], str)
-            dataset.shutdown()
+            for _ in range(2):
+                dataset.start()
+                for _ in range(3):
+                    dataset.update_cache()
+                    self.assertIsNotNone(dataset._cache[15])
+                    if isinstance(dataset._cache[15]["image"], np.ndarray):
+                        np.testing.assert_allclose(dataset._cache[15]["image"], dataset._cache[15]["label"])
+                    else:
+                        self.assertIsInstance(dataset._cache[15]["image"], str)
+                dataset.shutdown()
 
 
 if __name__ == "__main__":