Printing

Aside from plotting result, printing numbers is probably the main way you do science. (Or maybe your science consists entirely of listening to birdsong.) This tutorial can’t help make the numbers in your science better, but it can help you figure out more quickly if they’re good numbers or not.

Headings and colors

No one in their right mind would make a black and white plot these days, but it’s still pretty common to output monochrome text. Fair: color should be used sparingly. But when you do want a pop of color, Sciris has you covered. For example, you can easily make section headings to delineate large blocks of text:

import sciris as sc
import numpy as np

sc.heading('A very long green story')
string = '"Once upon a time, there was a story that began: '
sc.printgreen(sc.indent(string*20 + ' ...'))

sc.heading('Some very dull blue data')
sc.printblue(np.random.rand(10,6))




———————————————————————

A very long green story

———————————————————————



"Once upon a time, there was a story that began: "Once upon a time,

there was a story that began: "Once upon a time, there was a story

that began: "Once upon a time, there was a story that began: "Once

upon a time, there was a story that began: "Once upon a time, there

was a story that began: "Once upon a time, there was a story that

began: "Once upon a time, there was a story that began: "Once upon a

time, there was a story that began: "Once upon a time, there was a

story that began: "Once upon a time, there was a story that began:

"Once upon a time, there was a story that began: "Once upon a time,

there was a story that began: "Once upon a time, there was a story

that began: "Once upon a time, there was a story that began: "Once

upon a time, there was a story that began: "Once upon a time, there

was a story that began: "Once upon a time, there was a story that

began: "Once upon a time, there was a story that began: "Once upon a

time, there was a story that began:  ...







————————————————————————

Some very dull blue data

————————————————————————



[[0.90354608 0.806996   0.20244152 0.7629231  0.59344346 0.0691499 ]

 [0.14877491 0.63896495 0.77049046 0.46738765 0.86456043 0.94670538]

 [0.72303018 0.91593319 0.99154966 0.77686861 0.84885832 0.95770827]

 [0.71075814 0.2640391  0.20581044 0.60983694 0.35326234 0.28093268]

 [0.28580944 0.28302214 0.45492301 0.61778079 0.6654353  0.92033257]

 [0.32076897 0.21205642 0.33212397 0.75946709 0.51064089 0.19915571]

 [0.71619076 0.74516437 0.05974211 0.69307662 0.33619354 0.25877552]

 [0.37086355 0.63764479 0.52436766 0.49721602 0.55444415 0.84355198]

 [0.15977833 0.61394745 0.27247833 0.09079281 0.00520617 0.18020727]

 [0.64410244 0.17059352 0.63699504 0.71355348 0.737924   0.17399979]]

(Note: if you’re reading this on docs.sciris.org, there’s a little button at the top right where you can change to dark mode if you prefer – the colors might make more sense then!)

Incidentally, Sciris includes two functions for combining strings: sc.strjoin() and sc.newlinejoin(). These are just shortcuts to ', '.join() and '\n'.join(), respectively (plus automatic conversion to strings), but can make life easier, especially inside f-strings:

def get(key):
    my_dict = dict(key1=1, key2=2, key3=3)
    try:
        my_dict[key]
    except:
        errormsg = f'Invalid key {key}; must be {sc.strjoin(my_dict.keys())}, which have values:\n{sc.newlinejoin(my_dict.items())}'
        print(errormsg)

get('key4')
Invalid key key4; must be key1, key2, key3, which have values:
('key1', 1)
('key2', 2)
('key3', 3)

Printing objects

Let’s revisit our well-trodden sim:

import numpy as np
import matplotlib.pyplot as plt

class Sim:
    def __init__(self, n=10, n_factors=5):
        self.n = n
        self.n_factors = n_factors
        self.results = sc.objdict()
        self.ready = False
    
    def run(self):
        for i in range(self.n_factors):
            label = f'i={i+1}'
            result = np.random.randint(0, 10, self.n)**(i+1)
            self.results[label] = result
        self.ready = True
    
    def plot(self):
        plt.plot(self.results[:])

sim = Sim()
sim.run()

We can quickly view the full object with the “pretty representation”, or sc.pr():

sc.pr(sim)
<__main__.Sim at 0x7f8d0c3da510>
[<class '__main__.Sim'>]
————————————————————————————————————————————————————————————————————————
Methods:
  plot()                  run()                   
————————————————————————————————————————————————————————————————————————
        n: 10
n_factors: 5
    ready: True
  results: #0. 'i=1': array([4, 3, 6, 5, 8, 4, 1, 9, 3, 9])
           #1. 'i=2': array([25,  [...]
————————————————————————————————————————————————————————————————————————

Compare this to the standard but less informative dir():

dir(sim)
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__firstlineno__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getstate__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__static_attributes__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'n',
 'n_factors',
 'plot',
 'ready',
 'results',
 'run']

Trying to figure out what this means is a lot more work! For example, from dir(), you would’t know if run is an attribute (is it a flag indicating that the sim was run?) or a method.

In fact, this representation of an object is so useful, you can use it when you create the class. Then if you do print(sim), you’ll get the full representation rather than just the default (class name and memory address):

class PrettySim(sc.prettyobj): # This line is key, everything else is the same as before!
    def __init__(self, n=10, n_factors=5):
        self.n = n
        self.n_factors = n_factors
        self.results = sc.objdict()
        self.ready = False
    
    def run(self):
        for i in range(self.n_factors):
            label = f'i={i+1}'
            result = np.random.randint(0, 10, self.n)**(i+1)
            self.results[label] = result
        self.ready = True
    
    def plot(self):
        plt.plot(self.results[:])

sim = PrettySim()
sim.run()
print(sim)
<__main__.PrettySim at 0x7f8ccc8c97f0>
[<class '__main__.PrettySim'>, <class 'sciris.sc_printing.prettyobj'>]
————————————————————————————————————————————————————————————————————————
Methods:
  plot()                  run()                   
————————————————————————————————————————————————————————————————————————
        n: 10
n_factors: 5
    ready: True
  results: #0. 'i=1': array([6, 4, 6, 8, 5, 6, 3, 6, 5, 7])
           #1. 'i=2': array([49,  [...]
————————————————————————————————————————————————————————————————————————

(Some readers may question whether this representation is more useful than it is pretty. Point taken.)

Monitoring progress

What if you have a really slow task and you want to check progress? You can use sc.progressbar for that, which builds on the excellent package tqdm:

class SlowSim(PrettySim):
    
    def run_slow(self):
        for i in sc.progressbar(range(self.n_factors)): # This is the only change!
            sc.randsleep(0.2) # Make it slow
            label = f'i={i+1}'
            result = np.random.randint(0, 10, self.n)**(i+1)
            self.results[label] = result
        self.ready = True

slowsim = SlowSim()
slowsim.run_slow()

  0%|          | 0/5 [00:00<?, ?it/s]
 40%|████      | 2/5 [00:00<00:00,  5.22it/s]
 60%|██████    | 3/5 [00:00<00:00,  4.25it/s]
 80%|████████  | 4/5 [00:01<00:00,  3.71it/s]
100%|██████████| 5/5 [00:01<00:00,  3.27it/s]
100%|██████████| 5/5 [00:01<00:00,  3.62it/s]

Note that the progress bar looks better in a regular terminal than in Jupyter, and needless to say, it doesn’t look like anything in a static web page!