func (dt *Table) EmerHeaders() []string {
	hdrs := []string{}
	for i := range dt.Cols {
		tsr := dt.Cols[i]
		nm := dt.ColNames[i]
		nm = string([]byte{EmerHdrTypeToChar[tsr.DataType()]}) + nm
		if tsr.NumDims() == 1 {
			hdrs = append(hdrs, nm)
		} else {
			csh := etensor.NewShape(tsr.Shapes()[1:], nil, nil)  
			tc := csh.Len()
			nd := csh.NumDims()
			fnm := nm + fmt.Sprintf("[%v:", nd)
			dn := fmt.Sprintf("<%v:", nd)
			ffnm := fnm
			for di := 0; di < nd; di++ {
				ffnm += "0"
				dn += fmt.Sprintf("%v", csh.Dim(di))
				if di < nd-1 {
					ffnm += ","
					dn += ","
				}
			}
			ffnm += "]" + dn + ">"
			hdrs = append(hdrs, ffnm)
			for ti := 1; ti < tc; ti++ {
				idx := csh.Index(ti)
				ffnm := fnm
				for di := 0; di < nd; di++ {
					ffnm += fmt.Sprintf("%v", idx[di])
					if di < nd-1 {
						ffnm += ","
					}
				}
				ffnm += "]"
				hdrs = append(hdrs, ffnm)
			}
		}
	}
	return hdrs
}
EmerHeaders generates emergent DataTable header strings from the table.
These have full information about type and tensor cell dimensionality.
package etable
import (
	"bufio"
	"encoding/csv"
	"fmt"
	"io"
	"log"
	"math"
	"os"
	"strconv"
	"strings"
	"github.com/emer/etable/etensor"
	"github.com/goki/gi/gi"
	"github.com/goki/ki/kit"
)
type Delims int32
var KiT_Delims = kit.Enums.AddEnum(DelimsN, kit.NotBitFlag, nil)
func (ev Delims) MarshalJSON() ([]byte, error)  { return kit.EnumMarshalJSON(ev) }
func (ev *Delims) UnmarshalJSON(b []byte) error { return kit.EnumUnmarshalJSON(ev, b) }
const (
	Tab Delims = iota
	Comma
	Space
	Detect
	DelimsN
)
func (dl Delims) Rune() rune {
	switch dl {
	case Tab:
		return '\t'
	case Comma:
		return ','
	case Space:
		return ' '
	}
	return '\t'
}
const (
	Headers = true
	NoHeaders = false
)
func (dt *Table) SaveCSV(filename gi.FileName, delim Delims, headers bool) error {
	fp, err := os.Create(string(filename))
	defer fp.Close()
	if err != nil {
		log.Println(err)
		return err
	}
	bw := bufio.NewWriter(fp)
	err = dt.WriteCSV(bw, delim, headers)
	bw.Flush()
	return err
}
func (ix *IdxView) SaveCSV(filename gi.FileName, delim Delims, headers bool) error {
	fp, err := os.Create(string(filename))
	defer fp.Close()
	if err != nil {
		log.Println(err)
		return err
	}
	bw := bufio.NewWriter(fp)
	err = ix.WriteCSV(bw, delim, headers)
	bw.Flush()
	return err
}
func (dt *Table) OpenCSV(filename gi.FileName, delim Delims) error {
	fp, err := os.Open(string(filename))
	defer fp.Close()
	if err != nil {
		log.Println(err)
		return err
	}
	return dt.ReadCSV(bufio.NewReader(fp), delim)
}
func (ix *IdxView) OpenCSV(filename gi.FileName, delim Delims) error {
	fp, err := os.Open(string(filename))
	defer fp.Close()
	if err != nil {
		log.Println(err)
		return err
	}
	err = ix.Table.ReadCSV(bufio.NewReader(fp), delim)
	ix.Sequential()
	return err
}
func (dt *Table) ReadCSV(r io.Reader, delim Delims) error {
	cr := csv.NewReader(r)
	cr.Comma = delim.Rune()
	rec, err := cr.ReadAll()  
	if err != nil || len(rec) == 0 {
		return err
	}
	rows := len(rec)
	strow := 0
	if dt.NumCols() == 0 || DetectEmerHeaders(rec[0]) {
		sc, err := SchemaFromHeaders(rec[0], rec)
		if err != nil {
			log.Println(err.Error())
			return err
		}
		strow++
		rows--
		dt.SetFromSchema(sc, rows)
	}
	dt.SetNumRows(rows)
	for ri := 0; ri < rows; ri++ {
		dt.ReadCSVRow(rec[ri+strow], ri)
	}
	return nil
}
func (dt *Table) ReadCSVRow(rec []string, row int) {
	tc := dt.NumCols()
	ci := 0
	if rec[0] == "_D:" {  
		ci++
	}
	nan := math.NaN()
	for j := 0; j < tc; j++ {
		tsr := dt.Cols[j]
		_, csz := tsr.RowCellSize()
		stoff := row * csz
		for cc := 0; cc < csz; cc++ {
			str := rec[ci]
			if tsr.DataType() != etensor.STRING {
				if str == "" || str == "NaN" || str == "-NaN" || str == "Inf" || str == "-Inf" {
					tsr.SetNull1D(stoff+cc, true)  
					tsr.SetFloat1D(stoff+cc, nan)
				} else {
					tsr.SetString1D(stoff+cc, str)
				}
			} else {
				tsr.SetString1D(stoff+cc, str)
			}
			ci++
			if ci >= len(rec) {
				return
			}
		}
	}
}
func SchemaFromHeaders(hdrs []string, rec [][]string) (Schema, error) {
	if DetectEmerHeaders(hdrs) {
		return SchemaFromEmerHeaders(hdrs)
	}
	return SchemaFromPlainHeaders(hdrs, rec)
}
func DetectEmerHeaders(hdrs []string) bool {
	for _, hd := range hdrs {
		if hd == "" {
			continue
		}
		if hd == "_H:" {
			return true
		}
		if _, ok := EmerHdrCharToType[hd[0]]; !ok {  
			return false
		}
	}
	return true
}
func SchemaFromEmerHeaders(hdrs []string) (Schema, error) {
	sc := Schema{}
	for _, hd := range hdrs {
		if hd == "" || hd == "_H:" {
			continue
		}
		var typ etensor.Type
		typ, hd = EmerColType(hd)
		dimst := strings.Index(hd, "]<")
		if dimst > 0 {
			dims := hd[dimst+2 : len(hd)-1]
			lbst := strings.Index(hd, "[")
			hd = hd[:lbst]
			csh := ShapeFromString(dims)
			sc = append(sc, Column{Name: hd, Type: etensor.Type(typ), CellShape: csh})
			continue
		}
		dimst = strings.Index(hd, "[")
		if dimst > 0 {
			continue
		}
		sc = append(sc, Column{Name: hd, Type: etensor.Type(typ), CellShape: nil})
	}
	return sc, nil
}
var EmerHdrCharToType = map[byte]etensor.Type{
	'$': etensor.STRING,
	'%': etensor.FLOAT32,
	'#': etensor.FLOAT64,
	'|': etensor.INT64,
	'@': etensor.UINT8,
	'&': etensor.STRING,
	'^': etensor.BOOL,
}
var EmerHdrTypeToChar map[etensor.Type]byte
func init() {
	EmerHdrTypeToChar = make(map[etensor.Type]byte)
	for k, v := range EmerHdrCharToType {
		if k != '&' {
			EmerHdrTypeToChar[v] = k
		}
	}
	EmerHdrTypeToChar[etensor.INT8] = '@'
	EmerHdrTypeToChar[etensor.INT16] = '|'
	EmerHdrTypeToChar[etensor.UINT16] = '|'
	EmerHdrTypeToChar[etensor.INT32] = '|'
	EmerHdrTypeToChar[etensor.UINT32] = '|'
	EmerHdrTypeToChar[etensor.UINT64] = '|'
}
func EmerColType(nm string) (etensor.Type, string) {
	typ, ok := EmerHdrCharToType[nm[0]]
	if ok {
		nm = nm[1:]
	} else {
		typ = etensor.STRING  
	}
	return typ, nm
}
func ShapeFromString(dims string) []int {
	clni := strings.Index(dims, ":")
	nd, _ := strconv.Atoi(dims[:clni])
	sh := make([]int, nd)
	ci := clni + 1
	for i := 0; i < nd; i++ {
		dstr := ""
		if i < nd-1 {
			nci := strings.Index(dims[ci:], ",")
			dstr = dims[ci : ci+nci]
			ci += nci + 1
		} else {
			dstr = dims[ci:]
		}
		d, _ := strconv.Atoi(dstr)
		sh[i] = d
	}
	return sh
}
func SchemaFromPlainHeaders(hdrs []string, rec [][]string) (Schema, error) {
	sc := Schema{}
	nr := len(rec)
	for ci, hd := range hdrs {
		if hd == "" {
			hd = fmt.Sprintf("col_%d", ci)
		}
		dt := etensor.STRING
		nmatch := 0
		for ri := 1; ri < nr; ri++ {
			rv := rec[ri][ci]
			if rv == "" {
				continue
			}
			cdt := InferDataType(rv)
			switch {
			case cdt == etensor.STRING:  
				dt = cdt
				break
			case dt == cdt && (nmatch > 1 || ri == nr-1):  
				break
			case dt == cdt:  
				nmatch++
			case dt == etensor.STRING:  
				nmatch = 0
				dt = cdt
			case dt == etensor.INT64 && cdt == etensor.FLOAT64:  
				nmatch = 0
				dt = cdt
			}
		}
		sc = append(sc, Column{Name: hd, Type: dt, CellShape: nil})
	}
	return sc, nil
}
func InferDataType(str string) etensor.Type {
	if strings.Contains(str, ".") {
		_, err := strconv.ParseFloat(str, 64)
		if err == nil {
			return etensor.FLOAT64
		}
	}
	_, err := strconv.ParseInt(str, 10, 64)
	if err == nil {
		return etensor.INT64
	}
	_, err = strconv.ParseFloat(str, 64)
	if err == nil {
		return etensor.FLOAT64
	}
	return etensor.STRING
}
func (dt *Table) WriteCSV(w io.Writer, delim Delims, headers bool) error {
	ncol := 0
	var err error
	if headers {
		ncol, err = dt.WriteCSVHeaders(w, delim)
		if err != nil {
			log.Println(err)
			return err
		}
	}
	cw := csv.NewWriter(w)
	cw.Comma = delim.Rune()
	for ri := 0; ri < dt.Rows; ri++ {
		err = dt.WriteCSVRowWriter(cw, ri, ncol)
		if err != nil {
			log.Println(err)
			return err
		}
	}
	cw.Flush()
	return nil
}
func (ix *IdxView) WriteCSV(w io.Writer, delim Delims, headers bool) error {
	ncol := 0
	var err error
	if headers {
		ncol, err = ix.Table.WriteCSVHeaders(w, delim)
		if err != nil {
			log.Println(err)
			return err
		}
	}
	cw := csv.NewWriter(w)
	cw.Comma = delim.Rune()
	nrow := ix.Len()
	for ri := 0; ri < nrow; ri++ {
		err = ix.Table.WriteCSVRowWriter(cw, ix.Idxs[ri], ncol)
		if err != nil {
			log.Println(err)
			return err
		}
	}
	cw.Flush()
	return nil
}
func (dt *Table) WriteCSVHeaders(w io.Writer, delim Delims) (int, error) {
	cw := csv.NewWriter(w)
	cw.Comma = delim.Rune()
	hdrs := dt.EmerHeaders()
	nc := len(hdrs)
	err := cw.Write(hdrs)
	if err != nil {
		return nc, err
	}
	cw.Flush()
	return nc, nil
}
func (dt *Table) WriteCSVRow(w io.Writer, row int, delim Delims) error {
	cw := csv.NewWriter(w)
	cw.Comma = delim.Rune()
	err := dt.WriteCSVRowWriter(cw, row, 0)
	cw.Flush()
	return err
}
func (dt *Table) WriteCSVRowWriter(cw *csv.Writer, row int, ncol int) error {
	prec := -1
	if ps, ok := dt.MetaData["precision"]; ok {
		prec, _ = strconv.Atoi(ps)
	}
	var rec []string
	if ncol > 0 {
		rec = make([]string, 0, ncol)
	} else {
		rec = make([]string, 0)
	}
	rc := 0
	for i := range dt.Cols {
		tsr := dt.Cols[i]
		nd := tsr.NumDims()
		if nd == 1 {
			vl := ""
			if prec <= 0 || tsr.DataType() == etensor.STRING {
				vl = tsr.StringVal1D(row)
			} else {
				vl = strconv.FormatFloat(tsr.FloatVal1D(row), 'g', prec, 64)
			}
			if len(rec) <= rc {
				rec = append(rec, vl)
			} else {
				rec[rc] = vl
			}
			rc++
		} else {
			csh := etensor.NewShape(tsr.Shapes()[1:], nil, nil)  
			tc := csh.Len()
			for ti := 0; ti < tc; ti++ {
				vl := ""
				if prec <= 0 || tsr.DataType() == etensor.STRING {
					vl = tsr.StringVal1D(row*tc + ti)
				} else {
					vl = strconv.FormatFloat(tsr.FloatVal1D(row*tc+ti), 'g', prec, 64)
				}
				if len(rec) <= rc {
					rec = append(rec, vl)
				} else {
					rec[rc] = vl
				}
				rc++
			}
		}
	}
	err := cw.Write(rec)
	return err
}