What is Sciris?

Sciris is a library of tools that make writing scientific Python code easier and more pleasant. Built on top of NumPy and Matplotlib, it covers a wide range of common math, file I/O, and plotting operations, so you can get more done with less code. It's a "library of the gaps": the annoyances it addresses are each too small to need a dedicated library of their own, but common enough that together they add up. It's a bit like tidyverse for Python.

Why Sciris?

Brevity

Sciris packages common patterns that require multiple lines of code into single, simple functions: sc.parallelize() to run a function across CPUs, sc.save() and sc.load() for arbitrary Python objects, sc.surf3d() for a 3D plot. Less code to write means less code to debug.

Plain names

Functions are named after what they do, not after how they do it: sc.smooth(), sc.findnearest(), sc.safedivide(). Some names (sc.tic(), sc.toc(), sc.boxoff()) will look familiar if you have used MATLAB.

Forgiving defaults

Many Sciris functions take a die argument, so you can choose how strict you want to be. With die=False, Sciris warns and returns None so you can decide what to do next; with die=True, it raises. Either way, you write fewer try/except blocks.

Installation

Sciris requires Python 3.9 or later, and has no dependencies beyond the usual scientific Python stack.

pip install sciris          # using pip
uv add sciris               # using uv
conda install -c conda-forge sciris   # using conda

Then:

import sciris as sc

Doing science is left as an exercise to the reader.

Examples

sc.odict is a flexible container representing an associative array, with the best-of-all-worlds features of lists, dictionaries, and numeric arrays. It is based on OrderedDict, but supports integer indexing, key slicing, and item insertion. sc.objdict is the same, but also allows attribute-style access:

data = sc.objdict(a=[1,2,3], b=[4,5,6])

assert data.a == data['a'] == data[0]  # Refer to items by attribute, key, or index
assert data[:].sum() == 21             # You can sum a dict

for i, key, value in data.enumitems():
    print(f'Item {i} is named "{key}" and has value {value}')

# Item 0 is named "a" and has value [1, 2, 3]
# Item 1 is named "b" and has value [4, 5, 6]

To take a based-on-a-true-story example: if results is a dictionary of model runs, and each run is a dictionary with a data key, then getting the data from the first run is results[list(results.keys())[0]]['data'] with plain dictionaries, and results[0].data with an objdict.

Indexing arrays is a common task in NumPy, but it can be awkward when types do not quite match: floats versus integers, lists versus arrays. sc.findinds() finds matches anyway, and accepts multiple conditions:

sc.findinds([2,3,6,3], 3.0)  # Returns array([1, 3])

v = np.random.rand(100)
sc.findinds(v>0.4, v<0.6)    # Indices where both conditions hold

The first line is equivalent to np.nonzero(np.isclose(arr, val))[0], and the second to ((v>0.4)*(v<0.6)).nonzero()[0]. Related functions include sc.findnearest() (nearest value, whether or not it matches exactly), sc.findfirst(), sc.findlast(), and sc.smooth().

sc.save() and sc.load() handle arbitrary Python objects, including your own classes, so you can stop an analysis and pick it up later:

sc.save('results.obj', results)   # Save any Python object
results = sc.load('results.obj')  # Load it back

If you want a specific format, there is a function for that too: sc.savejson(), sc.loadjson(), sc.savetext(), sc.loadyaml(), sc.dataframe.read_csv(). And if you want to know later what produced a file, sc.savearchive() and sc.savefig() store the date, Python environment, and Git commit alongside the data or the figure, which sc.loadarchive() and sc.loadmetadata() read back.

Scientific workflows are often embarrassingly parallel, yet parallelizing them can still be a hurdle. sc.parallelize() is a shortcut to multiprocess.Pool() that accepts arguments in whichever form is most convenient:

def f(x, y):
    return x*y

out1 = sc.parallelize(f, iterarg=[(1,2), (2,3), (3,4)])
out2 = sc.parallelize(f, iterkwargs={'x':[1,2,3], 'y':[2,3,4]})
out3 = sc.parallelize(f, iterkwargs=[{'x':1, 'y':2},
                                     {'x':2, 'y':3},
                                     {'x':3, 'y':4}])

All three return [2, 6, 12]. By default the pool size is set from the number of CPUs available, but you can fix it, or allocate dynamically based on current load with sc.loadbalancer().

Sciris includes shortcuts for the parts of Matplotlib that are more fiddly than they need to be — date axes, tick formatting, mapping values onto colors:

sc.options(font='Raleway')                       # Set a custom font
x = sc.daterange('2022-06-01', '2022-12-31', as_date=True)  # Create dates
y = sc.smooth(np.random.rand(len(x))**2)*1000    # Create smoothed random numbers
c = sc.vectocolor(y, cmap='turbo')               # Set colors proportional to y

plt.scatter(x, y, c=c)  # Vanilla Matplotlib
sc.dateformatter()      # Automatic date formatting on the x-axis
sc.commaticks()         # Write 1000 as 1,000 rather than 1e3
sc.setylim()            # Set the y-axis to start at zero
sc.boxoff()             # Remove the top and right axis spines

Below are two functionally identical scripts: one written in plain Python (left), one using Sciris (right). Both sample random numbers from a user-defined function at several noise levels, save the intermediate results to disk, load them back, plot them in 3D, and report the elapsed time. The plain Python version takes about twice as many lines in total; counting only the lines that differ, and excluding comments and whitespace, it takes 33 where Sciris takes 7.

This is the output of the two scripts in the previous tab: plain Python on the left, Sciris on the right. The plots are identical apart from the colormap, which is one of several new ones that Sciris adds.

What's in it

A selection of the most commonly used functions. The API reference has the rest.

Containers

  • sc.odict(): dictionary that also acts like a list and an array
  • sc.objdict(): an odict that supports foo.bar as well as foo['bar']
  • sc.dataframe(): a pandas DataFrame with extra conveniences

Math and arrays

Files and versioning

Printing

Plotting

Parallelization and profiling

Other utilities

Citing Sciris

Sciris is described in the following paper, published in the Journal of Open Source Software:

Kerr CC, Sanz-Leon P, Abeysuriya RG, Chadderdon GL, Harbuz VS, Saidi P, Quiroga MM, Martin-Hughes R, Kelly SL, Cohen JA, Stuart RM, Nachesa A. Sciris: Simplifying scientific software in Python. Journal of Open Source Software 2023; 8(88):5076. DOI: 10.21105/joss.05076.

The citation is also available in BibTeX format.