import os
import pickle
import unittest

import numpy as np

import pygsti.protocols.estimate
from pygsti.extras import drift
from pygsti.modelpacks.legacy import stdQT_XYIMS
from ..report.reportBaseCase import ReportBaseCase
from ..testutils import compare_files, temp_files
from pygsti.baseobjs import Label

bLatex = bool('PYGSTI_LATEX_TESTING' in os.environ and
              os.environ['PYGSTI_LATEX_TESTING'].lower() in ("yes","1","true"))
try:
    import pandas
    bPandas = True
except ImportError:
    bPandas = False

class TestWorkspace(ReportBaseCase):

    def setUp(self):
        super(TestWorkspace, self).setUp()

        self.tgt = self.results.estimates['default'].models['target']
        self.ds = self.results.dataset.copy()
        self.ds.comment = "Hello\nWorld!" # for testing DS overview table
        self.mdl = self.results.estimates['default'].models['go0']
        self.gss = self.results.circuit_lists['final']

    def test_notebook_mode(self):
        wnb = pygsti.report.Workspace()
        with self.assertRaises(ValueError):
            wnb.init_notebook_mode()

    def test_caching(self):
        ws = pygsti.report.Workspace()
        def output_fn():
            return ws.NotApplicable() # a WorkspaceOutput obj
        key, result = ws.smartCache.cached_compute(output_fn, [])
        #print(key,result)

        ws.save_cache(temp_files + "/saved_workspace_testcache.pkl", show_unpickled=True)
        ws.load_cache(temp_files + "/saved_workspace_testcache.pkl")

    def test_plotly_pickling(self):
        import plotly.graph_objs as go

        # Create random data with numpy
        N = 1000
        random_x = np.random.randn(N)
        random_y = np.random.randn(N)

        # Create a trace
        trace = go.Scatter(
            x = random_x,
            y = random_y,
            mode = 'markers'
        )
        data = [trace]
        fig = go.Figure(data=[trace], layout={})
        pygsti.report.workspace.enable_plotly_pickling()

        #DEBUG
        #Import dill
        #dill.detect.trace(True)
        #dill.detect.badobjects(fig['frames'], depth=1)
        #print(dir(fig))
        #print("frames: ",type(fig['frames']))
        #s = pickle.dumps(fig['frames'])
        #print("layout",type(fig['layout']))
        #s = pickle.dumps(fig['layout'])
        #print("data",type(fig['data']))
        #s = pickle.dumps(fig['data'])

        s = pickle.dumps(fig)
        fig2 = pickle.loads(s)
        pygsti.report.workspace.disable_plotly_pickling()

        #Again for good measure
        pygsti.report.workspace.enable_plotly_pickling()
        pygsti.report.workspace.disable_plotly_pickling()

    def test_custom_digest(self):
        import hashlib
        M = hashlib.md5()
        ws = pygsti.report.Workspace()

        NA = ws.NotApplicable()
        pygsti.report.workspace.ws_custom_digest(M, NA)

        switchbd = ws.Switchboard(["My Switch"],[["On","Off"]],["buttons"])
        switchbd.add("mdl", [0])
        switchval = switchbd.mdl
        pygsti.report.workspace.ws_custom_digest(M, switchval)


    def test_table_creation(self):
        w = pygsti.report.Workspace()
        tbls = []; cr = None

        gsMultiSpam = self.mdl.copy()
        gsMultiSpam.povms['Msecondpovm'] = self.mdl.povms['Mdefault'].copy()
        gsTP = self.tgt.depolarize(0.01,0.01); gsTP.set_all_parameterizations("full TP")
        gsCPTP = self.tgt.depolarize(0.01,0.01); gsCPTP.set_all_parameterizations("CPTPLND")
        gsGM = self.mdl.depolarize(0.01,0.01); gsGM.basis = pygsti.baseobjs.Basis.cast("gm", 4)
        gsSTD = self.mdl.depolarize(0.01,0.01); gsSTD.basis = pygsti.baseobjs.Basis.cast("std", 4)
        gsQT = stdQT_XYIMS.target_model().depolarize(0.01,0.01)

        #Construct confidence regions
        def make_cr(mdl):
            hessian = pygsti.tools.logl_hessian(mdl, self.ds, self.gss,
                                                min_prob_clip=1e-4, prob_clip_interval=(-1e6,1e6),
                                                radius=1e-4)
            est = pygsti.protocols.estimate.Estimate.create_gst_estimate(None, mdl, None, []) #dummy w/out parent
            crfactory = pygsti.protocols.confidenceregionfactory.ConfidenceRegionFactory(
                parent=est, model_lbl="target", circuit_list_lbl=None,
                hessian=hessian, non_mark_radius_sq=0.0)
            crfactory.project_hessian('std')
            return crfactory.view(95)

        cr2 = make_cr(self.mdl)
        cr2TP = make_cr(gsTP)
        cr2CPTP = make_cr(gsCPTP)

        #----------------- Test table creation ---------------------------
        tbls.append( w.BlankTable() )
        tbls.append( w.SpamTable(self.mdl, ["mytitle"], "boxes", cr, True ) )
        tbls.append( w.SpamTable(gsTP, ["mytitle"], "boxes", cr2TP, True ) )
        tbls.append( w.SpamTable(self.mdl, ["mytitle"], "numbers", cr, False ) )
        with self.assertRaises(ValueError):
            w.SpamTable(self.mdl, ["mytitle"], "foobar", cr, False ) #invalid display_as

        tbls.append( w.SpamParametersTable(self.mdl, cr ) )
        tbls.append( w.SpamParametersTable(gsMultiSpam, cr ) )

        tbls.append( w.GatesTable(self.mdl, ["mytitle"], display_as="boxes", confidence_region_infos=cr ) )
        tbls.append( w.GatesTable(self.mdl, ["mytitle"], display_as="numbers", confidence_region_infos=cr ) )
        tbls.append( w.GatesTable(gsTP, ["mytitle"], display_as="numbers", confidence_region_infos=cr2TP ) )
        tbls.append( w.GatesTable(gsCPTP, ["mytitle"], display_as="numbers", confidence_region_infos=cr2CPTP ) )
        with self.assertRaises(ValueError):
            w.GatesTable(self.mdl, ["mytitle"], display_as="foobar", confidence_region_infos=cr )

        tbls.append( w.ChoiTable(self.mdl, ["mytitle"], cr ) )
        tbls.append( w.ChoiTable(gsQT, ["mytitle"], None ) )
        with self.assertRaises(ValueError):
            w.ChoiTable(self.mdl, ["mytitle"], cr, display=("foobar",) )

        tbls.append( w.GatesVsTargetTable(self.mdl, self.tgt, cr) )
        with self.assertRaises(ValueError):
            w.GatesVsTargetTable(self.mdl, self.tgt, cr, display=("foobar",) )

        tbls.append( w.SpamVsTargetTable(self.mdl, self.tgt, cr ) )
        tbls.append( w.ErrgenTable(self.mdl, self.tgt, cr, display_as="boxes", gen_type="logTiG") )
        tbls.append( w.ErrgenTable(self.mdl, self.tgt, cr, display_as="numbers", gen_type="logTiG") )
        tbls.append( w.ErrgenTable(self.mdl, self.tgt, cr, display_as="numbers", gen_type="logG-logT") )
        tbls.append( w.ErrgenTable(gsGM, self.tgt, cr, display_as="numbers", gen_type="logGTi") )
        #tbls.append( w.ErrgenTable(gsSTD, self.tgt, cr, display_as="numbers", gen_type="logGTi") )  # first el of basis must be I!!!
        tbls.append( w.ErrgenTable(gsQT, stdQT_XYIMS.target_model(), cr, display_as="numbers", gen_type="logGTi") )
        with self.assertRaises(ValueError):
            w.ErrgenTable(self.mdl, self.tgt, cr, display=('foobar',))
        with self.assertRaises(AssertionError):
            w.ErrgenTable(self.mdl, self.tgt, cr, display_as='foobar')
        with self.assertRaises(ValueError):
            w.ErrgenTable(self.mdl, self.tgt, cr, gen_type='foobar')

        tbls.append( w.GateDecompTable(self.mdl, self.tgt, cr) )

        #Polar plots don't work in latest plotly -- no 'r' variable -- TODO update (but these plots are unused) POLAR SKIP
        #tbls.append( w.GateEigenvalueTable(self.mdl, self.tgt, cr) )
        #tbls.append( w.GateEigenvalueTable(self.mdl, None, cr, display=("polar",) ) ) # polar with no target model
        tbls.append(w.GateEigenvalueTable(self.mdl, self.tgt, cr, display=("evdm","evinf","rel"),
                                          virtual_ops=[pygsti.circuits.Circuit([Label('Gxpi2',0), Label('Gxpi2',0)])]))
        with self.assertRaises(ValueError):
            tbls.append( w.GateEigenvalueTable(self.mdl, self.tgt, cr, display=("foobar",)) )

        tbls.append( w.DataSetOverviewTable(self.ds,max_length_list=[1,2,4,8]) )
        tbls.append( w.FitComparisonTable(self.gss.xs, self.results.circuit_lists['iteration'],
                                          [self.results.estimates['default'].models['iteration %d estimate' % k]
                                           for k in range(self.results.estimates['default'].num_iterations)], self.ds) )
        with self.assertRaises(ValueError):
            w.FitComparisonTable(self.gss.xs, self.results.circuit_lists['iteration'],
                                 [self.results.estimates['default'].models['iteration %d estimate' % k]
                                           for k in range(self.results.estimates['default'].num_iterations)],
                                 self.ds, objfn_builder="foobar")

        tbls.append( w.GaugeRobustErrgenTable(self.mdl, self.tgt) )

        prepStrs = self.results.circuit_lists['prep fiducials']
        effectStrs = self.results.circuit_lists['meas fiducials']
        tbls.append( w.CircuitTable((prepStrs,effectStrs),
                                    ["Prep.","Measure"], common_title="Fiducials"))

        metric_abbrevs = ["evinf", "evagi","evnuinf","evnuagi","evdiamond",
                          "evnudiamond", "inf","agi","trace","diamond","nuinf","nuagi",
                          "frob"]
        for metric in metric_abbrevs:
            tbls.append( w.GatesSingleMetricTable(
                metric, [self.mdl,self.mdl],[self.tgt,self.tgt], ['one','two'])) #1D
            tbls.append( w.GatesSingleMetricTable(
                metric, [[self.mdl],[self.mdl]],[[self.tgt],[self.tgt]],
                ['column one'], ['row one','row two'], op_label=Label("Gxpi2",0))) #2D
            tbls.append( w.GatesSingleMetricTable(
                metric, [self.mdl,None],[self.tgt,self.tgt], ['one','two'])) #1D w/None model

        tbls.append( w.StandardErrgenTable(4, "H", "pp") )  # 1Q
        tbls.append( w.StandardErrgenTable(9, "S", "gm") )   # qutrit
        tbls.append( w.StandardErrgenTable(16, "H", "gm") ) # 2Q
        tbls.append( w.StandardErrgenTable(64, "S", "pp") )  # >2Q (3Q)

        goparams = self.results.estimates['default'].goparameters['go0'].copy()
        goparams.update({'method': "LM", 'cptp_penalty_factor': 1.0}) # for coverage
        tbls.append( w.GaugeOptParamsTable(goparams) )
        tbls.append( w.GaugeOptParamsTable(False) )

        params = self.results.estimates['default'].parameters.copy()
        params['gaugeOptParams'] = goparams # add for coverate
        tbls.append( w.MetadataTable(self.mdl, params) )
        params['gaugeOptParams'] = [goparams] # can also be a list (for GOpt stages)
        tbls.append( w.MetadataTable(gsTP, params) )

        weirdGS = pygsti.models.modelconstruction.create_explicit_model_from_expressions(
            [('Q0','Q1')],['Gi'], ["I(Q0)"])
        #weirdGS.preps['rho1'] = pygsti.baseobjs.ComplementSPAMVec(weirdGS.preps['rho0'],[]) #num_params not implemented!
        weirdGS.povms['Mtensor'] = pygsti.modelmembers.povms.TensorProductPOVM([self.mdl.povms['Mdefault'], self.mdl.povms['Mdefault']])
        tbls.append( w.MetadataTable(weirdGS, params) )

        tbls.append( w.SoftwareEnvTable() )

        profiler = pygsti.baseobjs.Profiler()
        tbls.append( w.ProfilerTable(profiler,"time") )
        tbls.append( w.ProfilerTable(profiler,"name") )
        if profiler is not None:
            with self.assertRaises(ValueError):
                w.ProfilerTable(profiler,"foobar")

        #OLD tables
        #These don't look to be fully compatible with modern pygsti, so disable these tests.
        #tbls.append( w.old_RotationAxisVsTargetTable(self.mdl, self.tgt) )
        #tbls.append( w.old_GateDecompTable(self.mdl) )
        #tbls.append( w.old_RotationAxisTable(self.mdl) )


        #Now test table rendering in html
        for i,tbl in enumerate(tbls):
            print("Table: %s" % str(type(tbl)))
            out_html = tbl.render("html")
            #out_latex = tbl.render("latex") #not supported yet (figure formatting wants scratchdir)

            tbl.saveas(temp_files + "/saved_table_%d.html" % i)
            if bPandas:
                tbl.saveas(temp_files + "/saved_table_%d.pkl" % i)
            if bLatex:
                tbl.saveas(temp_files + "/saved_table_%d.tex" % i)
                tbl.saveas(temp_files + "/saved_table_%d.pdf" % i)
                tbl.set_render_options(render_includes=False, leave_includes_src=False) #dumb case where nothing is output
                tbl.render('latex')
            with self.assertRaises(ValueError):
                tbl.saveas(temp_files + "/saved_table_%d.foobar" % i) # Unknown file type

        w.save_cache(temp_files + "/saved_workspace_cache1.pkl", show_unpickled=True)
        w.load_cache(temp_files + "/saved_workspace_cache1.pkl")

        w2 = pygsti.report.Workspace(temp_files + "/saved_workspace_cache1.pkl") # init from cache

        #printOpenFiles()
        #print("PSUTIL open files (%d) = " % len(psutil.Process().open_files()), psutil.Process().open_files())
        #assert(False),"STOP"


    def test_plot_creation(self):
        w = pygsti.report.Workspace()
        prepStrs = self.results.circuit_lists['prep fiducials']
        effectStrs = self.results.circuit_lists['meas fiducials']
        non_gatestring_strs = [ 'GxString', 'GyString' ]

        plts = []
        plts.append( w.BoxKeyPlot(prepStrs, effectStrs) )
        plts.append( w.BoxKeyPlot(non_gatestring_strs, non_gatestring_strs) )
        plts.append( w.ColorBoxPlot(("chi2","logl"), self.gss, self.ds, self.mdl, box_labels=True,
                                    hover_info=True, sum_up=False, invert=False) )
        plts.append( w.ColorBoxPlot(("chi2","logl"), self.gss, self.ds, self.mdl, box_labels=False,
                                    hover_info=True, sum_up=True, invert=False) )
        #plts.append( w.ColorBoxPlot(("chi2","logl"), self.gss, self.ds, self.mdl, box_labels=False,
        #                            hover_info=True, sum_up=False, invert=True) )  # invert no longer supported
        plts.append( w.ColorBoxPlot(("chi2","logl"), self.gss, self.ds, self.mdl, box_labels=False,
                                    hover_info=True, sum_up=False, invert=False, typ="scatter") )

        mds = pygsti.data.MultiDataSet()
        mds.add_dataset("DS0",self.ds)
        mds.add_dataset("DS1",self.ds)
        dsc = pygsti.data.DataComparator([self.ds, self.ds], op_exclusions=['Gfoo'], op_inclusions=['Gxpi2', 'Gypi2', '[]'])
        dsc2 = pygsti.data.DataComparator(mds)
        dsc.run()
        dsc2.run()
        plts.append( w.ColorBoxPlot(("dscmp",), self.gss, None, self.mdl, dscomparator=dsc) ) # dscmp with 'None' dataset specified
        plts.append( w.ColorBoxPlot(("dscmp",), self.gss, None, self.mdl, dscomparator=dsc2) )

        tds = pygsti.io.read_time_dependent_dataset(compare_files + "/timeseries_data_trunc.txt")
        #OLD: driftresults = drift.do_basic_drift_characterization(tds)
        results_gst = drift.StabilityAnalyzer(tds, ids=True)
        results_gst.compute_spectra()
        results_gst.run_instability_detection(0.05)
        results_gst.run_instability_characterization(estimator='filter', modelselector=('default',()),verbosity=0)
        plts.append( w.ColorBoxPlot('driftsize', self.gss, self.ds, self.mdl, box_labels=False,
                                    hover_info=False, sum_up=True, invert=False, stabilityanalyzer=results_gst) )

        with self.assertRaises(ValueError):
            w.ColorBoxPlot(("foobar",), self.gss, self.ds, self.mdl)
        with self.assertRaises(ValueError):
            w.ColorBoxPlot(("chi2",), self.gss, self.ds, self.mdl, typ="foobar")

        gmx = np.identity(4,'d'); gmx[3,0] = 0.5
        plts.append(w.MatrixPlot(gmx, -1, 1, ['a','b','c','d'], ['e','f','g','h'], "X", "Y",
                                 colormap = pygsti.report.colormaps.DivergingColormap(vmin=-2, vmax=2)))
        plts.append( w.MatrixPlot(gmx, -1,1, ['a','b','c','d'], ['e','f','g','h'], "X", "Y",colormap=None))
        plts.append( w.GateMatrixPlot(gmx, -1,1, "pp", 'pp', "in", "out", box_labels=True) )

        #Polar plots don't work in latest plotly -- no 'r' variable -- TODO update (but these plots are unused) POLAR SKIP
        #plts.append( w.PolarEigenvaluePlot([np.linalg.eigvals(self.mdl.operations['Gx'])],["purple"],scale=1.5) )
        #plts.append( w.PolarEigenvaluePlot([np.linalg.eigvals(self.mdl.operations['Gx'])],["purple"],amp=2.0) )

        projections = np.zeros(16,'d')
        plts.append( w.ProjectionsBoxPlot(projections, "pp", box_labels=False) )
        plts.append( w.ProjectionsBoxPlot(projections, "gm", box_labels=True) )
        plts.append( w.ProjectionsBoxPlot(np.zeros(9,'d'), "gm", box_labels=True) )  # non-qubit case
        plts.append( w.ProjectionsBoxPlot(np.zeros(64,'d'), "gm", box_labels=True) ) # >2Q case

        choievals = np.array([-0.03, 0.02, 0.04, 0.98])
        choieb = np.array([0.05, 0.01, 0.02, 0.01])
        plts.append( w.ChoiEigenvalueBarPlot(choievals, None) )
        plts.append( w.ChoiEigenvalueBarPlot(choievals, choieb) )

        plts.append( w.FitComparisonBarPlot(self.gss.xs, self.results.circuit_lists['iteration'],
                                            [self.results.estimates['default'].models['iteration %d estimate' % k]
                                             for k in range(self.results.estimates['default'].num_iterations)], self.ds) )
        plts.append( w.GramMatrixBarPlot(self.ds,self.tgt) )

        plts.append( w.DatasetComparisonHistogramPlot(dsc, nbins=50, frequency=True,
                                                      log=True, display='pvalue'))
        plts.append( w.DatasetComparisonHistogramPlot(dsc, nbins=None, frequency=True,
                                                      log=False, display='llr'))
        with self.assertRaises(ValueError):
            w.DatasetComparisonHistogramPlot(dsc, nbins=50, frequency=True,
                                             log=False, display='foobar')

        # Note: RandomizedBenchmarkingPlot is tested in extras/test_rb.py

        #Now test table rendering in html
        for i,plt in enumerate(plts):
            print("Plot: %s" % str(type(plt)))
            out_html = plt.render("html")
            plt.saveas(temp_files + "/saved_plot_%d.html" % i)
            plt.saveas(temp_files + "/saved_plot_%d.pkl" % i) #Note: plots don't need Pandas for pickling
            if bLatex:
                plt.saveas(temp_files + "/saved_plot_%d.pdf" % i)
            with self.assertRaises(ValueError):
                plt.saveas(temp_files + "/saved_plot_%d.foobar" % i) # Unknown file type

        w.save_cache(temp_files + "/saved_workspace_cache1.pkl")
        w.load_cache(temp_files + "/saved_workspace_cache1.pkl")

        # printOpenFiles()
        # print("PSUTIL open files (%d) = " % len(psutil.Process().open_files()), psutil.Process().open_files())
        # assert(False),"STOP"


    def test_switchboard(self):
        w = pygsti.report.Workspace()
        ds = self.ds
        mdl = self.mdl
        gs2 = self.mdl.depolarize(op_noise=0.01, spam_noise=0.15)
        gs3 = self.mdl.depolarize(op_noise=0.011, spam_noise=0.1)

        switchbd = w.Switchboard(["dataset","model"],
                                 [["one","two"],["One","Two"]],
                                 ["dropdown","slider"])
        switchbd.add("ds",(0,))
        switchbd.add("mdl",(1,))
        switchbd["ds"][:] = [ds, ds]
        switchbd["mdl"][:] = [mdl, gs2]

        switchbd2 = w.Switchboard(["spamWeight"], [["0.0","0.1","0.2","0.5","0.9","0.95"]], ["slider"])

        tbl = w.SpamTable(switchbd["mdl"])
        plt = w.ColorBoxPlot(("chi2","logl"), self.gss, switchbd["ds"], switchbd["mdl"], box_labels=False)

        switchbd.render("html")
        switchbd2.render("html")
        tbl.render("html")
        plt.render("html")

        switchbd3 = w.Switchboard(["My Switch"],[["On","Off"]],["buttons"])
        switchbd3.add("mdl", [0])
        switchbd3.mdl[:] = [gs2,gs3]
        tbl2 = w.GatesVsTargetTable(switchbd3.mdl, self.tgt)

        switchbd3.render("html")
        tbl2.render("html")

        switchbd3.render("html")
        try:
            switchbd3.display()
        except:
            pass #might work, might not, depending on how much ipython support exists...

        switchbd4 = w.Switchboard(["My Numeric Slider"],[["0.0","1.0","1.5"]],["numslider"])
        switchbd5 = w.Switchboard(["My Switch"],[["On","Off"]],["buttons"],show="none") #test show=="none"
        with self.assertRaises(KeyError):
            switchbd5['key'] = 10 # can't add switched values like this.
        switchbd5.switchTypes[0] = "foobar" #mess with Switchboard internals to trigger errors below
        with self.assertRaises(ValueError):
            switchbd5.create_switch_change_handlerjs(0)
        with self.assertRaises(ValueError):
            switchbd5.create_switch_valuejs(0)
        with self.assertRaises(ValueError):
            switchbd5.render("html")

        switchbd4.render("html")
        #switchbd5.render("html")

        view1 = switchbd.view()
        view2 = switchbd.view(['dataset'])
        view3 = switchbd.view([True,False])
        view4 = pygsti.report.workspace.SwitchboardView(switchbd, show="all")
        view5 = pygsti.report.workspace.SwitchboardView(switchbd, show="none")

        try:
            view1.display()
        except:
            pass #might work, might not, depending on how much ipython support exists...

        #Test a switched WorkspaceTable & Plot & Text
        switchbd6 = w.Switchboard(["Plot type"],[["BoxeS","NumberS"]],["buttons"])
        switchbd6.add("typ", [0])
        switchbd6.add("strs", [0])
        switchbd6.typ[:] = ["boxes","numbers"]
        switchbd6.strs[:] = [ [('Gx',)], [('Gy',)] ]

        tbl = w.SpamTable(self.mdl, ["mytitle"], switchbd6.typ, None)
        tbl.saveas(temp_files + "/saved_switched_table.html", index=0)
        if bPandas: tbl.saveas(temp_files + "/saved_switched_table.pkl") # OK to not specify index in pkl case
        with self.assertRaises(ValueError):
            tbl.saveas(temp_files + "/saved_switched_table.html") # must supply index
        with self.assertRaises(ValueError):
            tbl.saveas(temp_files + "/saved_switched_table.tex") # must supply index

        plt = w.BoxKeyPlot(switchbd6.strs, switchbd6.strs)
        plt.saveas(temp_files + "/saved_switched_plot.html", index=0)
        plt.saveas(temp_files + "/saved_switched_plot.pkl") # OK to not specify index in pkl case
        with self.assertRaises(ValueError):
            plt.saveas(temp_files + "/saved_switched_plot.html") # must supply index
        with self.assertRaises(ValueError):
            plt.saveas(temp_files + "/saved_switched_plot.tex") # cannot save plots as LaTeX
        with self.assertRaises(ValueError):
            plt.saveas(temp_files + "/saved_switched_plot.pdf") # must supply index

        def textfn(s): return pygsti.report.textblock.ReportText(s, None)
        txt = pygsti.report.workspace.WorkspaceText(w, textfn, switchbd6.typ) # just "boxes" / "numbers" text
        txt.saveas(temp_files + "/saved_switched_text.html", index=0)
        txt.saveas(temp_files + "/saved_switched_text.pkl") # OK to not specify index in pkl case
        with self.assertRaises(ValueError):
            txt.saveas(temp_files + "/saved_switched_text.html") # must supply index
        with self.assertRaises(ValueError):
            txt.saveas(temp_files + "/saved_switched_text.tex") # must supply index


        # Test "Opaque figure" python rendering (fig w/out "pythonvalue")
        plt.figs[0].pythonvalue = None # mimic fig w/out pythonvalue
        py = plt.render("python")
        self.assertEqual( py['python']['index0']['value'], "Opaque Figure")


    def test_text_creation(self):
        printer = pygsti.baseobjs.VerbosityPrinter(1)
        printer.start_recording()
        printer.log("Hello World")
        lineinfo = printer.stop_recording()

        ws = pygsti.report.Workspace()
        texts = []
        texts.append( ws.StdoutText( lineinfo ) )

        #Now test rendering
        for i,text in enumerate(texts):
            print("Text: %s" % str(type(text)))
            out_html = text.render("html")
            text.saveas(temp_files + "/saved_text_%d.html" % i)
            text.saveas(temp_files + "/saved_text_%d.pkl" % i)
            if bLatex:
                text.saveas(temp_files + "/saved_text_%d.tex" % i)
                text.saveas(temp_files + "/saved_text_%d.pdf" % i)
                text.set_render_options(render_includes=False, leave_includes_src=False) #dumb case where nothing is output
                text.render("latex")
            with self.assertRaises(ValueError):
                text.saveas(temp_files + "/saved_text_%d.foobar" % i) # Unknown file type


    def test_workspaceoutput(self):
        ws = pygsti.report.Workspace()
        raw_wo = pygsti.report.workspace.WorkspaceOutput(ws)

        s = pickle.dumps(raw_wo)
        raw_wo2 = pickle.loads(s)

        #test some base class paths
        with self.assertRaises(ValueError):
            raw_wo.set_render_options(foobar="10")
        with self.assertRaises(NotImplementedError):
            raw_wo.render("html")
        with self.assertRaises(NotImplementedError):
            raw_wo.saveas("doesntmatter.html")

        raw_wo.set_render_options(global_requirejs=True)
        raw_wo._create_onready_handler("myJavascriptCode;", "MyID")

        try:
            raw_wo.display()
        except:
            pass #might work or not, depending on how much ipython is installed

    def test_notapplicable(self):
        ws = pygsti.report.Workspace()
        NA = ws.NotApplicable()
        NA.render("html")
        NA.render("latex")
        NA.render("python")
        with self.assertRaises(ValueError):
            NA.render("foobar")

    def test_plot_and_table_objects(self):
        ws = pygsti.report.Workspace()

        printer = pygsti.baseobjs.VerbosityPrinter(1)
        printer.start_recording()
        printer.log("Hello World (with $\\alpha$ math latex)")
        lineinfo = printer.stop_recording()
        strs = pygsti.circuits.to_circuits([(), ('Gx',), ('Gx', 'Gy')])

        table = ws.BlankTable()
        plot = ws.BoxKeyPlot(strs, strs)
        text = ws.StdoutText( lineinfo )
        objs = [table, plot, text] # list of "representative" objects for testing

        for obj in objs:
            print("Testing ",type(obj))
            obj.set_render_options(precision=4,
                                   output_dir=temp_files) # needed for html rendering
            obj.render("html")

            obj.set_render_options(switched_item_mode='foobar')
            with self.assertRaises(ValueError):
                obj.render("html") #invalid switched-item-mode
            if bLatex:
                with self.assertRaises(ValueError):
                    obj.render("latex") #invalid switched-item-mode
            if bPandas:
                with self.assertRaises(ValueError):
                    obj.render("python") #invalid switched-item-mode

            for mode in ('separate files','inline'):
                obj.set_render_options(switched_item_mode=mode,
                                       autosize="continual") #pick options not used elsewhere
                obj.render("html")
                if bLatex:  obj.render("latex")
                if bPandas: obj.render("python")

            with self.assertRaises(NotImplementedError):
                obj.render("foobar")

        #Individual item tests
        with self.assertRaises(ValueError):
            plot.set_render_options(valign="foobar")
            plot.render("html")


    def test_plot_helpers(self):
        from pygsti.report import plothelpers as ph

        self.assertEqual(ph._eformat(0.1, "compacthp"),".10")
        self.assertEqual(ph._eformat(1.0, "compacthp"),"1.0")
        self.assertEqual(ph._eformat(5.2, "compacthp"),"5.2")
        self.assertEqual(ph._eformat(63.2, "compacthp"),"63")
        self.assertEqual(ph._eformat(2.1e-4, "compacthp"),"2m4")
        self.assertEqual(ph._eformat(2.1e+4, "compacthp"),"2e4")
        self.assertEqual(ph._eformat(-3.2e-4, "compacthp"),"-3m4")
        self.assertEqual(ph._eformat(-3.2e+4, "compacthp"),"-3e4")
        self.assertEqual(ph._eformat(4e+40, "compacthp"),"*40")
        self.assertEqual(ph._eformat(6e+102, "compacthp"),"B")
        self.assertEqual(ph._eformat(10, "compacthp"),"10")
        self.assertEqual(ph._eformat(1.234, 2),"1.23")
        self.assertEqual(ph._eformat(-1.234, 2),"-1.23")
        self.assertEqual(ph._eformat(-1.234, "foobar"), "-1.234") #just prints in general format

        subMxs = np.nan * np.ones((3,3,2,2),'d') # a 3x3 grid of 2x2 matrices
        nBoxes, dof_per_box = ph._compute_num_boxes_dof(subMxs, sum_up=True, element_dof=1)
        self.assertEqual(nBoxes, 0)
        self.assertEqual(dof_per_box, None)

        subMxs[0,0,1,1] = 1.0 # matrix [0,0] has a single non-Nan element
        subMxs[0,2,0,1] = 1.0
        subMxs[0,2,1,1] = 1.0 # matrix [0,2] has a two non-Nan elements

        # now the mxs that aren't all-NaNs don't all have the same # of Nans => warning
        #self.assertWarns(ph._compute_num_boxes_dof, subMxs, sum_up=True, element_dof=1)
        ph._compute_num_boxes_dof( subMxs, sum_up=True, element_dof=1) # Python2.7 doesn't always warn...


    def test_plot_basefns(self):
        #Tests non-covered boundary cases for plot-supporting functions
        import pygsti.report.colormaps
        import pygsti.report.workspaceplots

        colormap = pygsti.report.colormaps.DivergingColormap(0, 10)
        mx = np.identity(2,'d')
        mxs = [ [mx, mx],
                [mx, mx] ]
        gstrs = pygsti.circuits.to_circuits([(), ('Gx',)])

        # ---- _nested_color_boxplot ----
        pygsti.report.workspaceplots._nested_color_boxplot(mxs, colormap)

        # ---- _summable_color_boxplot ----
        pygsti.report.workspaceplots._summable_color_boxplot(
            mxs, gstrs, gstrs, 'Xlbl','Ylbl', colormap) #circuit labels

        pygsti.report.workspaceplots._summable_color_boxplot(
            mxs, ['xlbl1','xlbl2'], ['ylbl1','ylbl2'],
            'Xlbl','Ylbl', colormap,
            sum_up=True, hover_info=True) #sumup test

        pygsti.report.workspaceplots._summable_color_boxplot(
            mxs, ['xlbl1','xlbl2'], ['ylbl1','ylbl2'],
            'Xlbl','Ylbl', colormap,
            sum_up=True, hover_info=False) #no hover info

        pygsti.report.workspaceplots._summable_color_boxplot(
            mxs, ['xlbl1','xlbl2'], ['ylbl1','ylbl2'],
            'Xlbl','Ylbl', colormap,
            sum_up=False, hover_info=False) # no hover info or sum_up

        # invert is no longer an option
        #pygsti.report.workspaceplots._summable_color_boxplot(
        #    mxs, ['xlbl1','xlbl2'], ['ylbl1','ylbl2'],
        #    'Xlbl','Ylbl', colormap,
        #    sum_up=True, invert=True) #ignores invert

        # ----- _circuit_color_boxplot -----
        germs = preps = effects = gstrs
        plaquettes = {}
        fidpairs = {(i,j): (prep, meas) for j,prep in enumerate(preps) for i,meas in enumerate(effects)}
        for L in [1,2]:
            for germ in germs:
                plaquettes[(L, germ)] = pygsti.circuits.GermFiducialPairPlaquette(germ, L, fidpairs)
        gss = pygsti.circuits.PlaquetteGridCircuitStructure(plaquettes, [1, 2], germs, 'L', 'germ')
        gss2 = gss.copy()
        
        #cls = type('DummyClass', pygsti.baseobjs.LsGermsStructure.__bases__, dict(pygsti.baseobjs.LsGermsStructure.__dict__))
        #gss3.__class__ = cls  # mimic a non-LsGermsStructure object when we don't actually have any currently (HACK)
        #assert(not isinstance(gss3, pygsti.baseobjs.LsGermsStructure))

        pygsti.report.workspaceplots._circuit_color_boxplot(
            gss, mxs, colormap, sum_up=True)
        pygsti.report.workspaceplots._circuit_color_boxplot(
            gss, mxs, colormap, sum_up=False)
        pygsti.report.workspaceplots._circuit_color_boxplot(
            gss2, mxs, colormap, sum_up=True)
        pygsti.report.workspaceplots._circuit_color_boxplot(
            gss2, mxs, colormap, sum_up=False)


        # ----- _circuit_color_scatterplot -----
        colormap = pygsti.report.colormaps.SequentialColormap(0,10)
        pygsti.report.workspaceplots._circuit_color_scatterplot(
            gss, mxs, colormap, sum_up=True)
        pygsti.report.workspaceplots._circuit_color_scatterplot(
            gss, mxs, colormap, hover_info=False) # no hoverinfo
        pygsti.report.workspaceplots._circuit_color_scatterplot(
            gss2, mxs, colormap, hover_info=True) # gss2 case
        pygsti.report.workspaceplots._circuit_color_scatterplot(
            gss2, mxs, colormap, hover_info=True) # gss2 case
        pygsti.report.workspaceplots._circuit_color_scatterplot(
            gss, mxs, colormap, sum_up=True, addl_hover_submxs={'qty': mxs} ) # just reuse mxs
        pygsti.report.workspaceplots._circuit_color_scatterplot(
            gss, mxs, colormap, sum_up=False, addl_hover_submxs={'qty': mxs} ) # just reuse mxs
        #pygsti.report.workspaceplots._circuit_color_scatterplot(
        #    gss3, mxs, colormap, sum_up=True, hover_info=True) # gss3 case
        #pygsti.report.workspaceplots._circuit_color_scatterplot(
        #    gss3, mxs, colormap, sum_up=False, hover_info=True) # gss3 case


        # ---- _circuit_color_histogram ----
        negmx = -1*np.ones((2,2),'d')
        mxs_allneg = [ [negmx, negmx],
                         [negmx, negmx] ]
        pygsti.report.workspaceplots._circuit_color_histogram(
            gss, mxs_allneg, colormap) # when there's no counts to plot

        # ---- _opmatrix_color_boxplot ----
        pygsti.report.workspaceplots._opmatrix_color_boxplot(
            np.identity(4,'d'), -1.0, 1.0, mx_basis_x="pp", mx_basis_y="gm")
          # test weird case when there's different bases on diff axes

        # ---- _matrix_color_boxplot ----
        pygsti.report.workspaceplots._matrix_color_boxplot(
            np.identity(16,'d'), colormap=colormap, grid="black:2") # test "gridcolor:linewidth" format of grid argument.


if __name__ == "__main__":
    unittest.main(verbosity=2)
