Repository: wright-group/WrightTools

Issue:
from_solis:  import fails when no metadata
It is possible to export asc image from Solis software without metadata (clearly this is not preferred, but it should be handled gracefully).  

from_solis assumes "Data and Time" field exists in metadata (for attrs timestamp).  Fall back on file creation date as an alternative.   


Discussion:
@ddkohler can set up the other camera if this needs to be studied with an Andor without interrupting experiments

Resolving patch:
diff --git a/WrightTools/data/_solis.py b/WrightTools/data/_solis.py
--- a/WrightTools/data/_solis.py
+++ b/WrightTools/data/_solis.py
@@ -7,6 +7,7 @@
 import os
 import pathlib
 import time
+import warnings
 
 import numpy as np
 
@@ -43,8 +44,20 @@ def from_Solis(filepath, name=None, parent=None, verbose=True) -> Data:
 
     Returns
     -------
-    data
+    data : WrightTools.Data
         New data object.
+        Channels: `signal`.  If exposure time is in metadata, signal is given as a count rate (Hz).
+        Variables, Axes: `yindex` and `xindex` (no grating) or `wm` (grating)
+
+    Notes
+    -----
+    When exporting as ascii, including metadata is optional.
+    It is _strongly recommended_ that you include metadata in exports.
+    Metadata informs the image creation date, exposure time, and axes.
+    However, if metadata is not present, this importer will make its best guesses to populate these fields accurately.
+
+    Saving processed data (e.g. vertically-binned data) in Solis software can remove/omit important metadata, so we advise exporting the raw camera images.
+
     """
     # parse filepath
     filestr = os.fspath(filepath)
@@ -102,9 +115,17 @@ def get_frames(f, arr, axis0):
                 attrs[key.strip()] = val.strip()
 
     f.close()
-    created = attrs["Date and Time"]  # is this UTC?
-    created = time.strptime(created, "%a %b %d %H:%M:%S %Y")
-    created = timestamp.TimeStamp(time.mktime(created)).RFC3339
+
+    try:
+        created = attrs["Date and Time"]  # is this UTC?
+        created = time.strptime(created, "%a %b %d %H:%M:%S %Y")
+        created = timestamp.TimeStamp(time.mktime(created)).RFC3339
+    except KeyError:  # use file creation time
+        created = os.stat(filepath).st_mtime
+        created = timestamp.TimeStamp(created).RFC3339
+        warnings.warn(
+            f"{filepath.name} has no 'Date and Time' field: using file modified time instead: {created}"
+        )
 
     kwargs = {"name": name, "kind": "Solis", "source": filestr, "created": created}
     if parent is None:
@@ -113,7 +134,15 @@ def get_frames(f, arr, axis0):
         data = parent.create_data(**kwargs)
 
     axis0 = np.array(axis0)
-    if float(attrs["Grating Groove Density (l/mm)"]) == 0:
+    try:
+        groove_density = float(attrs["Grating Groove Density (l/mm)"])
+    except KeyError:  # assume no grating
+        warnings.warn(
+            f"{filepath.name} has no 'Grating Groove Density (1/mm)' field: guessing x axis units."
+        )
+        groove_density = isinstance(axis0[0], float)
+
+    if groove_density == 0:
         xname = "xindex"
         xunits = None
     else:
@@ -135,9 +164,17 @@ def get_frames(f, arr, axis0):
         axes = ["frame"] + axes
 
     data.transform(*axes)
-    arr /= float(attrs["Exposure Time (secs)"])
-    # signal has units of Hz because time normalized
-    data.create_channel(name="signal", values=arr, signed=False, units="Hz")
+    try:
+        exposure_time = float(attrs["Exposure Time (secs)"])
+        if exposure_time == 0:
+            raise ZeroDivisionError
+        arr /= exposure_time
+    except (KeyError, ZeroDivisionError) as e:  # do not normalize
+        warnings.warn(f"{filepath.name} camera signal cannot be given as a count rate.")
+        data.create_channel(name="signal", values=arr, signed=False)
+    else:
+        # signal has units of Hz because time normalized
+        data.create_channel(name="signal", values=arr, signed=False, units="Hz")
 
     for key, val in attrs.items():
         data.attrs[key] = val


Test patch: