text: /******************************************************************************
   *
   * Copyright (c) 2019, the Perspective Authors.
   *
   * This file is part of the Perspective library, distributed under the terms of
   * the Apache License 2.0.  The full license can be found in the LICENSE file.
   *
   */
  #ifdef PSP_ENABLE_PYTHON
  #include <perspective/python/fill.h>
  #include <perspective/python/numpy.h>
  
  using namespace perspective;
  
  namespace perspective {
  namespace numpy {
  
      const std::vector<std::string> NumpyLoader::DATE_UNITS = {"[D]", "[W]", "[M]", "[Y]"};
  
      NumpyLoader::NumpyLoader(py::object accessor)
          : m_init(false)
          , m_accessor(accessor) {}
  
      NumpyLoader::~NumpyLoader() {}
  
      void
      NumpyLoader::init() {
          m_names = make_names();
          m_types = make_types();
          m_init = true;
      }
  
      std::vector<t_dtype> 
      NumpyLoader::reconcile_dtypes(const std::vector<t_dtype>& inferred_types) const {
          PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
          std::vector<t_dtype> reconciled_types;
          std::uint32_t num_columns = m_names.size();
  
          // Get numpy dtypes as string so we can tell the difference between dates and datetimes
          std::vector<std::string> str_dtypes = m_accessor.attr("types")().cast<std::vector<std::string>>();
  
          for (auto i = 0; i < num_columns; ++i) {
              std::string numpy_type_as_string = str_dtypes[i];
              t_dtype numpy_type = m_types[i];
              t_dtype inferred_type = inferred_types[i];
  
              // Check whether column is a date or a datetime
              if(numpy_type_as_string.find("datetime64") != std::string::npos) {
                  for (const std::string& unit : DATE_UNITS) {
                      if (numpy_type_as_string.find(unit) != std::string::npos) {
                          inferred_type = DTYPE_DATE;
                      }
                  }
              }
  
              // Otherwise, numpy type takes precedence unless date/object - need specificity of inferred type
              if (inferred_type == DTYPE_DATE || numpy_type == DTYPE_OBJECT) {
                  reconciled_types.push_back(inferred_type);
              } else {
                  reconciled_types.push_back(numpy_type);
              }
          }
  
          return reconciled_types;
      };
  
      std::vector<std::string>
      NumpyLoader::names() const {
          PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
          return m_names;
      }
  
      std::vector<t_dtype>
      NumpyLoader::types() const {
          PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
          return m_types;
      }
  
      std::uint32_t
      NumpyLoader::row_count() const {
          PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
          return m_accessor.attr("row_count")().cast<std::uint32_t>();
      }
  
      void
      NumpyLoader::fill_table(t_data_table& tbl, const t_schema& input_schema,
          const std::string& index, std::uint32_t offset, std::uint32_t limit, bool is_update) {
          PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
          bool implicit_index = false;
          std::vector<std::string> col_names(input_schema.columns());
          std::vector<t_dtype> data_types(input_schema.types());
  
          for (auto cidx = 0; cidx < col_names.size(); ++cidx) {
              auto name = col_names[cidx];
              auto type = data_types[cidx];
  
              if (name == "__INDEX__") {
                  implicit_index = true;
                  std::shared_ptr<t_column> pkey_col_sptr = tbl.add_column_sptr("psp_pkey", type, true);
                  fill_column(tbl, pkey_col_sptr, "__INDEX__", type, cidx, is_update);
                  tbl.clone_column("psp_pkey", "psp_okey");
                  continue;
              }
  
              auto col = tbl.get_column(name);
              fill_column(tbl, col, name, type, cidx, is_update);
          }
  
          // Fill index column - recreated every time a `t_data_table` is created.
          if (!implicit_index) {
              if (index == "") {
                  // Use row number as index if not explicitly provided or provided with `__INDEX__`
                  auto key_col = tbl.add_column("psp_pkey", DTYPE_INT32, true);
                  auto okey_col = tbl.add_column("psp_okey", DTYPE_INT32, true);
  
                  for (std::uint32_t ridx = 0; ridx < tbl.size(); ++ridx) {
                      key_col->set_nth<std::int32_t>(ridx, (ridx + offset) % limit);
                      okey_col->set_nth<std::int32_t>(ridx, (ridx + offset) % limit);
                  }
              } else {
                  tbl.clone_column(index, "psp_pkey");
                  tbl.clone_column(index, "psp_okey");
              }
          }
      }
  
      
      void 
      NumpyLoader::fill_column(t_data_table& tbl, std::shared_ptr<t_column> col, const std::string& name, t_dtype type, std::uint32_t cidx, bool is_update) {
          PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
  
          // Use name index instead of column index - prevents off-by-one errors with the "index" column.
          auto name_it = std::find(m_names.begin(), m_names.end(), name); 
          
          // If the column name is not in the dataset, return and move on.
          if (name_it == m_names.end()) {
              return;
          }
  
          auto nidx = std::distance(m_names.begin(), name_it);
  
          // np_dtype is one of the integer/float/bool dtypes, or `DTYPE_OBJECT`.
          t_dtype np_dtype = m_types[nidx];
         
          py::dict source = m_accessor.attr("_get_numpy_column")(name);
          py::array array = source["array"].cast<py::array>();
          py::array_t<std::uint64_t> mask = source["mask"].cast<py::array_t<std::uint64_t>>();
          std::uint64_t* mask_ptr = (std::uint64_t*) mask.data();
          std::size_t mask_size = mask.size();
  
          // Check array dtype to make sure that `deconstruct_numpy` didn't cast it to an object.
          if (array.dtype().kind() == 'O') {
              fill_column_iter(array, tbl, col, name, DTYPE_OBJECT, type, cidx, is_update);
              return;
          }
  
          // Datetimes are not trivially copyable - they are float64 values that need to be read as int64
          if (type == DTYPE_TIME || type == DTYPE_DATE) {
              fill_column_iter(array, tbl, col, name, np_dtype, type, cidx, is_update);
              fill_validity_map(col, mask_ptr, mask_size, is_update);
              return;
          }
          
           /**
           * Catch common type mismatches and fill iteratively when a numpy dtype is of greater bit width than the Perspective t_dtype:
           * - when `np_dtype` is int64 and `t_dtype` is `DTYPE_INT32` or `DTYPE_FLOAT64`
           * - when `np_dtype` is int32 and `t_dtype` is `DTYPE_INT64` or `DTYPE_FLOAT64`, which can happen on windows where np::int_ is int32
           * - when `np_dtype` is float64 and `t_dtype` is `DTYPE_INT32` or `DTYPE_INT64`
           * - when `type` is float64 and `np_dtype` is `DTYPE_FLOAT32` or `DTYPE_FLOAT64`
           * 
           * These errors occur frqeuently when a Table is created from non-numpy data or schema, then updated with a numpy array.
           * In these cases, the `t_dtype` of the Table supercedes the array dtype.
           */
          bool should_iter = (np_dtype == DTYPE_INT64 && (type == DTYPE_INT32 || type == DTYPE_FLOAT64)) || \
              (np_dtype == DTYPE_INT32 && (type == DTYPE_INT64 || type == DTYPE_FLOAT64)) || \
              (np_dtype == DTYPE_FLOAT64 && (type == DTYPE_INT32 || type == DTYPE_INT64)) || \
              (type == DTYPE_INT64 && (np_dtype == DTYPE_FLOAT32 || np_dtype == DTYPE_FLOAT64));
  
          if (should_iter) {
              // Skip straight to numeric fill
              fill_numeric_iter(array, tbl, col, name, np_dtype, type, cidx, is_update);
              return;
          }
  
          bool copy_status = try_copy_array(array, col, np_dtype, type, 0);
  
          // Iterate if copy is not supported for the numpy array
          if (copy_status == t_fill_status::FILL_FAIL) {
              fill_column_iter(array, tbl, col, name, np_dtype, type, cidx, is_update);
          }
          
          // Fill validity map using null mask
          fill_validity_map(col, mask_ptr, mask_size, is_update);
      }
  
      template <typename T>
      void
      NumpyLoader::fill_object_iter(t_data_table& tbl, std::shared_ptr<t_column> col, const std::string& name, t_dtype np_dtype, t_dtype type, std::uint32_t cidx, bool is_update) {
          t_uindex nrows = col->size();
  
          for (auto i = 0; i < nrows; ++i) {
              t_val item = m_accessor.attr("marshal")(cidx, i, type);
  
              if (item.is_none()) {
                  if (is_update) {
                      col->unset(i);
                  } else {
                      col->clear(i);
                  }
                  continue;
              }
  
              col->set_nth(i, item.cast<T>());
          }
      }
  
      // Add explicit instantiations for int32, int64, and float64 as they have promotion logic
      template <>
      void
      NumpyLoader::fill_object_iter<std::int32_t>(t_data_table& tbl, std::shared_ptr<t_column> col, const std::string& name, t_dtype np_dtype, t_dtype type, std::uint32_t cidx, bool is_update) {
          t_uindex nrows = col->size();
  
          for (auto i = 0; i < nrows; ++i) {
              t_val item = m_accessor.attr("marshal")(cidx, i, type);
  
              if (item.is_none()) {
                  if (is_update) {
                      col->unset(i);
                  } else {
                      col->clear(i);
                  }
                  continue;
              }
  
              double fval = item.cast<double>();
              if (!is_update && (fval > 2147483647 || fval < -2147483648)) {
                  binding::WARN("Promoting column `%s` to float from int32", name);
                  tbl.promote_column(name, DTYPE_FLOAT64, i, true);
                  col = tbl.get_column(name);
                  type = DTYPE_FLOAT64;
                  col->set_nth(i, fval);
              } else if (!is_update && isnan(fval)) {
                  binding::WARN("Promoting column `%s` to string from int32", name);
                  tbl.promote_column(name, DTYPE_STR, i, false);
                  col = tbl.get_column(name);
                  fill_object_iter<std::string>(
                      tbl, col, name, np_dtype, DTYPE_STR, cidx, is_update);
                  return;
              } else {
                  col->set_nth(i, static_cast<std::int32_t>(fval));
              }
          }
      }
  
      template <>
      void
      NumpyLoader::fill_object_iter<std::int64_t>(t_data_table& tbl, std::shared_ptr<t_column> col, const std::string& name, t_dtype np_dtype, t_dtype type, std::uint32_t cidx, bool is_update) {
          t_uindex nrows = col->size();
  
          for (auto i = 0; i < nrows; ++i) {
              t_val item = m_accessor.attr("marshal")(cidx, i, type);
  
              if (item.is_none()) {
                  if (is_update) {
                      col->unset(i);
                  } else {
                      col->clear(i);
                  }
                  continue;
              }
  
              double fval = item.cast<double>();
              if (isnan(fval)) {
                  binding::WARN("Promoting column `%s` to string from int64", name);
                  tbl.promote_column(name, DTYPE_STR, i, false);
                  col = tbl.get_column(name);
                  fill_object_iter<std::string>(
                      tbl, col, name, np_dtype, DTYPE_STR, cidx, is_update);
                  return;
              } else {
                  col->set_nth(i, static_cast<std::int64_t>(fval));
              }
          }
      }
  
      template <>
      void
      NumpyLoader::fill_object_iter<double>(t_data_table& tbl, std::shared_ptr<t_column> col, const std::string& name, t_dtype np_dtype, t_dtype type, std::uint32_t cidx, bool is_update) {
          t_uindex nrows = col->size();
  
          for (auto i = 0; i < nrows; ++i) {
              t_val item = m_accessor.attr("marshal")(cidx, i, type);
  
              if (item.is_none()) {
                  if (is_update) {
                      col->unset(i);
                  } else {
                      col->clear(i);
                  }
                  continue;
              }
  
              bool is_float = py::isinstance<py::float_>(item);
              bool is_numpy_nan = is_float && npy_isnan(item.cast<double>());
              if (!is_float || is_numpy_nan) {
                  binding::WARN("Promoting column `%s` to string from float64", name);
                  tbl.promote_column(name, DTYPE_STR, i, false);
                  col = tbl.get_column(name);
                  fill_object_iter<std::string>(
                      tbl, col, name, np_dtype, DTYPE_STR, cidx, is_update);
                  return;
              }
              col->set_nth(i, item.cast<double>());
          }
      }
  
      // Must be below `fill_object_iter` explicit instantiations.
      void
      NumpyLoader::fill_column_iter(const py::array& array, t_data_table& tbl, std::shared_ptr<t_column> col, const std::string& name, t_dtype np_dtype, t_dtype type, std::uint32_t cidx, bool is_update) {
          PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
          // Numpy arrays are guaranteed to be continous and valid by the time they enter this block,
          // but if they're of dtype object, then we need to pass it through `m_accessor.marshal`.
          switch (type) {
              case DTYPE_TIME: {
                  // covers dtype `datetime64[us/ns/ms/s]`, date strings, and integer timestamps in ms or s since epoch
                  if (np_dtype != DTYPE_TIME) {
                      fill_object_iter<std::int64_t>(tbl, col, name, np_dtype, type, cidx, is_update);
                  } else {
                      fill_datetime_iter(array, tbl, col, name, np_dtype, type, cidx, is_update);
                  }
              } break;
              case DTYPE_DATE: {
                  // `datetime.date` objects or `datetime64[D/W/M/Y]`, always fill by using `marshal`.
                  fill_date_iter(col, name, np_dtype, type, cidx, is_update);
              } break;
              case DTYPE_BOOL: {
                  if (np_dtype == DTYPE_OBJECT) {
                      fill_object_iter<bool>(tbl, col, name, np_dtype, type, cidx, is_update);
                  } else {
                      fill_bool_iter(array, tbl, col, name, np_dtype, type, cidx, is_update);
                  }
              } break;
              case DTYPE_STR: {
                  // dtype `U`
                  fill_object_iter<std::string>(tbl, col, name, np_dtype, type, cidx, is_update);
              } break;
              default: {
                  // dtype `i/u/f` - fill_numeric_iter checks again for `dtype=object`
                  fill_numeric_iter(array, tbl, col, name, np_dtype, type, cidx, is_update);
                  break;
              }
          }
      }
  
      // `array.dtype=datetime64[ns/us/ms/s]`
      void
      NumpyLoader::fill_datetime_iter(const py::array& array, t_data_table& tbl, std::shared_ptr<t_column> col, 
          const std::string& name, t_dtype np_dtype, t_dtype type, std::uint32_t cidx, bool is_update) {
          PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
          t_uindex nrows = col->size();
  
          // read the array as a double array because of `numpy.nat`
          double* ptr = (double*) array.data();
  
          for (auto i = 0; i < nrows; ++i) {
              std::int64_t item = ptr[i]; // Perspective stores datetimes using int64
              col->set_nth(i, item);
          }
      }
  
      void
      NumpyLoader::fill_date_iter(std::shared_ptr<t_column> col, const std::string& name, t_dtype np_dtype, t_dtype type, std::uint32_t cidx, bool is_update) {
          PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
          t_uindex nrows = col->size();
  
          for (auto i = 0; i < nrows; ++i) {
              t_val item = m_accessor.attr("marshal")(cidx, i, type);
  
              if (item.is_none()) {
                  if (is_update) {
                      col->unset(i);
                  } else {
                      col->clear(i);
                  }
                  continue;
              }
  
              auto date_components = item.cast<std::map<std::string, std::int32_t>>();
              // date_components["month"] should be [0-11]
              t_date dt = t_date(date_components["year"], date_components["month"], date_components["day"]);
              col->set_nth(i, dt);
          }
      }
  
      void
      NumpyLoader::fill_bool_iter(const py::array& array, t_data_table& tbl, std::shared_ptr<t_column> col, const std::string& name, t_dtype np_dtype, t_dtype type, std::uint32_t cidx, bool is_update) {
          PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
          t_uindex nrows = col->size();
  
          // handle Nan/None in boolean array with dtype=object
          if (np_dtype == DTYPE_OBJECT) {
              // handle object arrays
              fill_object_iter<bool>(tbl, col, name, np_dtype, type, cidx, is_update); 
          } else {
              bool* ptr = (bool*) array.data(); 
  
              for (auto i = 0; i < nrows; ++i) {
                  bool item = ptr[i];
                  col->set_nth(i, item);
              }
          }
      }
  
      void 
      NumpyLoader::fill_numeric_iter(const py::array& array, t_data_table& tbl, std::shared_ptr<t_column> col, const std::string& name, t_dtype np_dtype, t_dtype type, std::uint32_t cidx, bool is_update) {
          PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
          t_uindex nrows = col->size();
          const void* ptr = array.data();
  
          // We fill by object when `np_dtype`=object, or if there are type mismatches between `np_dtype` and `type`.
          bool types_mismatched = (np_dtype == DTYPE_INT64 && (type == DTYPE_INT32 || type == DTYPE_FLOAT64)) || \
              (np_dtype == DTYPE_INT32 && (type == DTYPE_INT64 || type == DTYPE_FLOAT64)) || \
              (np_dtype == DTYPE_FLOAT64 && (type == DTYPE_INT32 || type == DTYPE_INT64)) || \
              (type == DTYPE_INT64 && (np_dtype == DTYPE_FLOAT32 || np_dtype == DTYPE_FLOAT64));
  
          if (types_mismatched || np_dtype == DTYPE_OBJECT) {
              switch (type) {
                  case DTYPE_UINT8: {
                      fill_object_iter<std::uint8_t>(tbl, col, name, np_dtype, type, cidx, is_update);
                      return;
                  } break;
                  case DTYPE_UINT16: {
                      fill_object_iter<std::uint16_t> (tbl, col, name, np_dtype, type, cidx, is_update);
                      return;
                  } break;
                  case DTYPE_UINT32: {
                      fill_object_iter<std::uint32_t>(tbl, col, name, np_dtype, type, cidx, is_update);
                      return;
                  } break;
                  case DTYPE_UINT64: {
                      fill_object_iter<std::uint64_t>(tbl, col, name, np_dtype, type, cidx, is_update);
                      return;
                  } break;
                  case DTYPE_INT8: {
                      fill_object_iter<std::int8_t>(tbl, col, name, np_dtype, type, cidx, is_update);
                      return;
                  } break;
                  case DTYPE_INT16: {
                      fill_object_iter<std::int16_t>(tbl, col, name, np_dtype, type, cidx, is_update);
                      return;
                  } break;
                  case DTYPE_INT32: {
                      fill_object_iter<std::int32_t>(tbl, col, name, np_dtype, type, cidx, is_update);
                      return;
                  } break;
                  case DTYPE_INT64: {
                      fill_object_iter<std::int64_t>(tbl, col, name, np_dtype, type, cidx, is_update);
                      return;
                  } break;
                  case DTYPE_FLOAT32: {
                      fill_object_iter<float>(tbl, col, name, np_dtype, type, cidx, is_update);
                      return;
                  } break;
                  case DTYPE_FLOAT64: {
                      fill_object_iter<double>(tbl, col, name, np_dtype, type, cidx, is_update);
                      return;
                  } break;
                  default:
                      PSP_COMPLAIN_AND_ABORT("Unable to fill non-numeric column `"  + name + "` in `fill_numeric_iter`.")
              }
          }
  
          // Iterate through the C++ array and try to cast. Array is guaranteed to be of the correct dtype and consistent in its values.
          for (auto i = 0; i < nrows; ++i) {
              if (isnan(((double*) ptr)[i]) || npy_isnan(((double*) ptr)[i])) {
                  if (is_update) {
                      col->unset(i);
                  } else {
                      col->clear(i);
                  }
                  continue;
              }
  
              switch (type) {
                  case DTYPE_UINT8: {
                      col->set_nth(i, ((std::uint8_t*)ptr)[i]);
                  } break;
                  case DTYPE_UINT16: {
                      col->set_nth(i, ((std::uint16_t*)ptr)[i]);
                  } break;
                  case DTYPE_UINT32: {
                      col->set_nth(i, ((std::uint32_t*)ptr)[i]);
                  } break;
                  case DTYPE_UINT64: {
                      col->set_nth(i, ((std::uint64_t*)ptr)[i]);
                  } break;
                  case DTYPE_INT8: {
                      col->set_nth(i, ((std::int8_t*)ptr)[i]);
                  } break;
                  case DTYPE_INT16: {
                      col->set_nth(i, ((std::int16_t*)ptr)[i]);
                  } break;
                  case DTYPE_INT32: {
                      // No need for promotion logic if array is consistent
                      col->set_nth<std::int32_t>(i, ((std::int32_t*) ptr)[i]);
                  } break;
                  case DTYPE_INT64: {
                      col->set_nth<std::int64_t>(i, ((std::int64_t*) ptr)[i]);
                  } break;
                  case DTYPE_FLOAT32: {
                      col->set_nth(i, ((float*) ptr)[i]);
                  } break;
                  case DTYPE_FLOAT64: {
                      col->set_nth<double>(i, ((double*)ptr)[i]);
                  } break;
                  default:
                      break;
              }
          }
      }
  
      /******************************************************************************
       *
       * Copy numpy arrays into columns
       */
      t_fill_status 
      NumpyLoader::try_copy_array(const py::array& src, std::shared_ptr<t_column> dest, t_dtype np_dtype, t_dtype type, const std::uint64_t offset) {
          PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
          std::int64_t length = src.size();
  
          switch (np_dtype) {
              case DTYPE_BOOL:
              case DTYPE_UINT8: {
                  copy_array_helper<std::uint8_t>(src.data(), dest, offset);
              } break;
              case DTYPE_UINT16: {
                  copy_array_helper<std::uint16_t>(src.data(), dest, offset);
              } break;
              case DTYPE_UINT32: {
                  copy_array_helper<std::uint32_t>(src.data(), dest, offset);
              } break;
              case DTYPE_UINT64: {
                  copy_array_helper<std::uint64_t>(src.data(), dest, offset);
              } break;
              case DTYPE_INT8: {
                  copy_array_helper<std::int8_t>(src.data(), dest, offset);
              } break;
              case DTYPE_INT16: {
                  copy_array_helper<std::int16_t>(src.data(), dest, offset);
              } break;
              case DTYPE_INT32: {
                  copy_array_helper<std::int32_t>(src.data(), dest, offset);
              } break;
              case DTYPE_INT64: {
                  copy_array_helper<std::int64_t>(src.data(), dest, offset);
              } break;
              case DTYPE_FLOAT32: {
                  copy_array_helper<float>(src.data(), dest, offset);
              } break;
              case DTYPE_FLOAT64: {
                  copy_array_helper<double>(src.data(), dest, offset);
              } break;
              default: {
                  return t_fill_status::FILL_FAIL;
              }
          }
  
          return t_fill_status::FILL_SUCCESS;
      }
  
      void
      NumpyLoader::fill_validity_map(
          std::shared_ptr<t_column> col, std::uint64_t* mask_ptr, std::size_t mask_size, bool is_update) {
          // Validity map needs to be filled each time - None/np.nan/float('nan') might not have been parsed correctly
          col->valid_raw_fill();
  
          if (mask_size > 0) {
              for (auto i = 0; i < mask_size; ++i) {
                  std::uint64_t idx = mask_ptr[i];
                  if (is_update) {
                      col->unset(idx);
                  } else {
                      col->clear(idx);
                  }
              }
          }
      }
  
      template <typename T>
      void copy_array_helper(const void* src, std::shared_ptr<t_column> dest, const std::uint64_t offset) {
          std::memcpy(dest->get_nth<T>(offset), src, dest->size() * sizeof(T));
      }
  
      /******************************************************************************
       *
       * Generate metadata for numpy arrays
       */
      std::vector<std::string>
      NumpyLoader::make_names() {
          auto data = m_accessor.attr("data")();
          auto py_names = m_accessor.attr("names")().cast<std::vector<std::string>>();
  
          // Match names to dataset - only keep names that are present in dataset.
          // The `m_names` variable is used internally to access the numpy arrays
          // containing each column. On first-time load, `m_names` contains
          // every name in the dataset. On update, `m_names` is recalculated to
          // only include columns that are present in the update dataset.
          std::vector<std::string> names;
          for (const auto& name : py_names) {
              if (data.contains(py::str(name))) {
                  names.push_back(name);
              }
          }
  
          return names;
      }
  
      std::vector<t_dtype>
      NumpyLoader::make_types() {
          std::vector<t_dtype> rval;
          
          auto data = m_accessor.attr("data")();
          for (const auto& name : m_names) {
              // Access each array by name to guarantee ordered access.
              py::array array = py::array::ensure(data[py::str(name)]);
  
              if (!array) {
                  PSP_COMPLAIN_AND_ABORT("Perspective does not support the mixing of ndarrays and lists.");
              }
  
              // can't use isinstance on datetime/timedelta array, so check the dtype
              char dtype_code = array.dtype().kind();
  
              if (dtype_code == 'M') {
                  // datetime64
                  rval.push_back(DTYPE_TIME);
                  continue;
              } else if (dtype_code == 'm') {
                  // coerce timedelta to string
                  rval.push_back(DTYPE_STR);
                  continue;
              }
  
              // isinstance checks equality of underlying dtype, not just pointer equality
              if (py::isinstance<py::array_t<std::uint8_t>>(array)) {
                  rval.push_back(DTYPE_UINT8);
              } else if (py::isinstance<py::array_t<std::uint16_t>>(array)) {
                  rval.push_back(DTYPE_UINT16);
              } else if (py::isinstance<py::array_t<std::uint32_t>>(array)) {
                  rval.push_back(DTYPE_UINT32);
              } else if (py::isinstance<py::array_t<std::uint64_t>>(array)) {
                  rval.push_back(DTYPE_UINT64);
              } else if (py::isinstance<py::array_t<std::int8_t>>(array)) {
                  rval.push_back(DTYPE_INT8);
              } else if (py::isinstance<py::array_t<std::int16_t>>(array)) {
                  rval.push_back(DTYPE_INT16);
              } else if (py::isinstance<py::array_t<std::int32_t>>(array)) {
                  rval.push_back(DTYPE_INT32);
              } else if (py::isinstance<py::array_t<std::int64_t>>(array)) {
                  rval.push_back(DTYPE_INT64);
              } else if (py::isinstance<py::array_t<float>>(array)) {
                  rval.push_back(DTYPE_FLOAT32);
              } else if (py::isinstance<py::array_t<double>>(array)) {
                  rval.push_back(DTYPE_FLOAT64);
              } else if (py::isinstance<py::array_t<bool>>(array)) {
                  rval.push_back(DTYPE_BOOL);
              } else {
                  // DTYPE_OBJECT defers to the inferred type: this allows parsing of datetime strings, boolean strings, etc.
                  rval.push_back(DTYPE_OBJECT);
              }
          }
  
          return rval;
      }
      
  } // namespace numpy
  } // namespace perspective
  #endif