Skip to main content

ESCLAB

Engineering Simulation and Control Lab

This software provides an environment for time-series simulation and control of complex multi-component systems. The approach used here relies on iterative (successive substitution) solving to converge on values for a given step, allowing compartmentalization of calculations between individual system components.

The simulation is configured and managed in Python scripts.

A graphical plotting interface provides real-time simulation results and a network topology rendering tool.

The software is structured to support both research and teaching activities and is used in the upper-graduate course "ME 964: Simulation and Optimal Control of Energy Systems" at the University of Wisconsin-Madison.

Installation

  1. Download and install a Python package manager. This program was developed using miniconda, and it's recommended for environment creation and maintenance.

  2. Download and install VS Code (or your favorite Python IDE, though VS Code is currently recommended)

  3. Open a command console and create a new conda environment. You can call this what you'd like, but I assume the name 'esclab_dev' in the documentation:

# Create and activate a new conda environment
conda create -n esclab_dev python=3.13
conda activate esclab_dev

Installing from source code

Use this option when you plan on extending the component or model libraries with code that will closely integrate. This allows direct modification of the ESCLab source code and incorporation of the latest updates that are pushed to the GitHub repository.

  1. Change to a convenient location for the repository. A path with no spaces is recommended.
cd C:\repositories
  1. Collect the ESCLab package source code. If you're using git, clone the repository. Otherwise, you can download and unzip the repository.
# Clone
git clone https://github.com/uw-esolab/esclab.git
  1. Install the ESCLab project to the python package you just created.
# The -e flag makes the installation editable.
pip install -e .
# ESCLab currently requires the most recent version of EESLib (published by uw-esolab). 
# Clone and install the editable version to this conda environment. 
# Make sure C:\repositories is replace with the path to your repository directory.
pip install -e C:\repositories\eeslib
# Ensure the editable version is correctly installed in esclab_dev.
python -c "import eeslib; print(eeslib.__file__)"

Installing from the package manager (PyPi)

Do this instead of the source code option if you're happy with a relatively stable release of the ESCLab core source code and will only be developing models and components for your own use without intending to share them as part of the ESCLab distribution.

Following step #3 above:

  1. Install the package using pip:
pip install esclab

Getting started

  • Source code for constructing and simulating models is found in the /src/esclab directory
  • Components are defined in the /src/esclab/components folder
  • Models are defined in the /src/esclab/models folder

To create a new model, prepare one or more component files that contain instance(s) of Component classes that are units in the system you want to model. Connect components together in a model file.

Templates and examples for Components and Models are provided in their respective folders.

The stepping loop stays in the model script. When model.step() advances time to or beyond model.settings.stop_time, it automatically calls model.finalize(). This calls each component's postsim_calcs() once, in component execution order, after the final timestep's calculations, historian logging, and plot logging. These hooks run even if that timestep reaches its iteration limit without converging. An exception interrupting timestep execution does not automatically finalize the simulation. Hooks run inside the final step() call, before any subsequent code in the caller's loop body.

To extend a simulation after reaching its current stop time, disable automatic finalization before stepping:

model.settings.auto_postsim_calcs = False
while model.time < model.settings.stop_time:
    model.step()

model.settings.stop_time = extended_stop_time
while model.time < model.settings.stop_time:
    model.step()

model.finalize()

Alternatively, re-enable auto_postsim_calcs before stepping through the final extension. Explicit finalize() ignores this setting and can also finish a run stopped early, after at least one completed timestep. Successful repeated calls do nothing. Once finalization starts, further stepping is forbidden; create a new model for a new run. If a hook raises, its exception propagates and finalization cannot be retried, since earlier hooks may already have produced side effects. Post-simulation results should generally be stored as separate component attributes: changes to outputs in these hooks do not rewrite history or plots.

  • template_model.py provides a template for building new system models
  • template_component.py provides a template for a component library
  • sample_circuit_secsub.py gives and example of a simple solve loop using successive substitution (no coupled equations)
  • sample_circuit.py gives an equivalent example using the coupled equation approach. sample_circuit_tee.py, and sample_circuit_hilopass.py also give examples of solving coupled equations for increasingly complex topologies.

The main calculation and plotting scripts are:

  • simulate.py | core simulation engine
  • network_topology.py | tools for detecting, organizing, and rendering coupled network systems
  • online_plotter.py | Qt-based window for real-time plotting

Plotters accept time_units to display the time axis in nsec, msec, sec (the default), min, hr, day, week, or year. Each plotter can use different units, including plots rendered after the simulation with show_live=False. Axis labels, tooltips, and status strips use the selected units; simulation time and plotted y-values remain unchanged. A year is defined as 365 days.

model.add_plotter([model.storage.charge], time_units="hr")
model.add_plotter_simstats(time_units="min", show_live=False)

Inputs, outputs, and parameters support NumPy arrays shaped (m,) or (1, m) as well as scalars. Assign arrays directly to a port or parameter and use .v to access the underlying array. Connections apply relaxation and convergence tolerances element by element; every element must converge. Array parameter defaults are independent for each component instance. The historian stores a copy of each array output per step, preserving its original shape. General (n, m) matrix signals are outside this vector plotting API.

Array plotters default to a 5th–95th percentile envelope with the ensemble mean. Use array_config to select a mode and configure its appearance:

model.add_plotter(model.source.ambient,
    array_config={"mode": "fill_between", "percentiles": (10, 90),
                  "fill_alpha": 0.25, "mean_width": 3})
model.add_plotter(model.battery.grid,
    array_config={"mode": "spaghetti", "spaghetti_alpha": 0.1,
                  "spaghetti_width": 1, "show_mean": True})
model.add_plotter(model.battery.soc,
    array_config={"mode": "indices", "indices": [0, 5, 12]})

Percentiles use the 0–100 scale and are computed across realizations at each time step. Spaghetti mode draws all members by default; indices can limit it to a subset. Indices are zero based; a single integer also works. In envelope mode, indices adds selected member traces over the band. show_mean defaults to True for envelopes and spaghetti, and False for selected indices. Existing line_colors, line_styles, and line_weights configure each signal; member spaghetti lines use that signal's color with their own alpha and width. mean_width=None uses the signal's line weight.

A single configuration dict applies to every array signal on both axes; scalar signals retain their usual behavior. For different configurations per signal, pass a list ordered as y1 followed by y2 (use None for defaults):

model.add_plotter([model.building.indoor, model.source.ambient],
    array_config=[{"mode": "fill_between", "percentiles": (5, 95)},
                  {"mode": "indices", "indices": [0, 2]}])

Live and deferred plots support all modes. Each signal must keep a fixed member count throughout a plot's history; empty arrays and matrices raise a clear error. Array tooltips show the mean, the configured lower and upper percentiles, minimum, and maximum across all members at the hovered timestep. Statistics are computed only when the tooltip moves to another timestep, using that step's values. Array histories retain every member, so memory use scales with steps times members. See src/esclab/models/sample_array_io.py for a simulation example.

Click a legend label or its colored swatch to hide or restore that trace. Hidden labels are dimmed. Spaghetti plots have separate entries for the realization lines as a group and the mean; percentile bands and selected indices can also be toggled separately. These controls work during live simulation and after it finishes. Hidden traces continue recording data and restore their history when shown again. "Fit All" scales to visible traces; if an axis has no visible finite values, its current Y range is preserved. Explicit Y limits remain in effect.

Set model.settings.global_plotter_args to supply defaults for all new data plotters, including simulation-statistics plotters. Any plotter keyword option except y1 and y2 is allowed. Explicit per-plotter values override these settings, including None to restore automatic axis limits or tab titles. Omitted options without a global setting retain their built-in defaults.

model.settings.global_plotter_args = {
    "show_live": False,
    "time_units": "hr",
    "nmax_points": 500,
    "y1lim": (0, 100),
}
model.add_plotter([model.storage.charge])
model.add_plotter([model.storage.flow_in], time_units="min", y1lim=None)
model.add_plotter_simstats(y1lim=None)

You can also assign Model.Settings.global_plotter_args before creating models to set class defaults. Each new model receives an independent copy of that dictionary. Changes to a model's settings affect subsequently added plotters.

Appearance options work in global_plotter_args, add_plotter, add_plotter_simstats, and directly in OnlinePlotter:

model.settings.global_plotter_args.update(
    font_size=12, font_color="black", bg_color="white",
    line_weights=2, line_styles="--", line_colors="tab:blue",
)
model.add_plotter(
    [model.storage.charge], [model.storage.flow_in],
    line_weights=[[3], [1]], line_styles=[["s--"], [":"]],
    line_colors=[["tab:red"], ["#377eb8"]],
)

font_size sets the initial size in points for ticks, axis labels, legends, and plot tooltips. The font buttons continue to adjust all plots, preserving their initial size differences. font_color sets axis, legend, and tooltip text colors; bg_color sets the plot background. Colors accept Matplotlib names, hex strings, or RGB(A) tuples. For line_weights, line_styles, and line_colors, a scalar or single-entry list applies to every series. Supply either a flat list ordered by y1 first, then y2, or nested [[y1 values], [y2 values]] lists matching each axis's series count (the groups may have different lengths). Use an empty second group when there are no y2 series. Use RGB(A) tuples for individual colors within these lists. Styles accept Matplotlib format strings, including "--", ":", "-.", "s--", and marker-only "o". Explicit line_colors override colors embedded in format strings. None restores the default palette or styles (solid for y1, dashed for y2); the default width is 2.

Grid options also work through direct keywords and global defaults. All three default to True: grid enables the grid overall, grid_horizontal enables horizontal lines, and grid_vertical enables vertical lines. grid=False hides both directions regardless of the directional settings.

model.settings.global_plotter_args.update(grid=True, grid_vertical=False)
model.add_plotter([model.storage.charge])  # horizontal grid only
model.add_plotter([model.storage.flow_in], grid=False)  # no grid

Definitions:

Term Definition
Component Building block for a system model. Components take input and compute output values, and they connect with other components
Model Collection of one or more components together into a simulated system
Simulation Time-dependent evaluation of a model
Step Calculations done at a specific value in time. A simulation contains many steps
Coupled equations Equations that are designated to be solved together using matrix inversion, outside of the normal successive-substitution method
Absolute tolerance Absolute difference between connection values on succesive iterations
Relative tolerance Difference relative to the magnitude of the last connection value
Learning rate Fraction of the difference between the new and old computed values to apply when iterating

Uploading a new version to PyPi (developers only!)

To upload a new version of ESCLab to Pypi, follow the steps outlined in the Python packaging tutorial.

The preferred packaging tool is setuptools.

The most relevant steps are as follows:

  1. Don't forget to update the code version number in pyproject.toml and in the src/__init__.py file.

  2. Open a command window and navigate to the esclab directory, such as

    cd C:\repositories\esclab
    
  3. Ensure the build and packaging tools are installed in the Conda environment that you're using. The preferred method will install a developer tools, including build, pytest, and twine, specified in the pyproject.toml file in the esclab directory:

    pip install .[dev]
    

    Alternatively, you can manually install packages:

    python -m pip install --upgrade build 
    python -m pip install --upgrade twine
    
  4. Build the Python distributable

    python -m build
    

    This should create a folder dist/ that contains a wheel (.whl) and tar.gz file.

  5. Upload the file to Pypi. You will need to have first created a username and API token, following the packing tutorial instructions. If uploading to the production server, use the command:

    twine upload dist/*
    

    If you receive the warning WARNING This environment is not supported for trusted publishing, you can ignore it.

    If using the test server, use the command:

    python -m twine upload --repository testpypi dist/*
    

    To test installation from the test server in a new, temporary environment:

    conda create -n test_esclab python=3.13
    conda activate test_esclab
    pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ esclab==0.0.1
    
    python -c "import esclab; print(esclab.__file__);"
    >> C:\Users\username\AppData\Local\miniconda3\envs\test_esclab\Lib\site-packages\esclab\__init__.py
    
    conda activate base
    conda env remove -n test_esclab
    
  6. To install the package, activate the Conda environment (e.g., conda activate esclab_dev), and install. If running from the production environment use:

    pip install esclab
    

    If you have previous versions of esclab already installed, force use of the most recent version using:

    pip install --force-reinstall --upgrade esclab
    

Authors

Lead author: Mike Wagner | Associate Professor, University of Wisconsin-Madison | GitHub: uw-esolab | Profile

Contributors:

Metadata

Release files for esclab 0.0.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for esclab 0.0.7
File Size Uploaded Provenance
esclab-0.0.7.tar.gz 101.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for esclab 0.0.7
File Interpreter ABI Platform
esclab-0.0.7-py3-none-any.whl Python 3 none any Details

Total release size: 191.7 kB

Release files / esclab-0.0.7.tar.gz

Download URL esclab-0.0.7.tar.gz
Size 101.5 kB
Tags Source
SHA-256 checksum
How to use checksums
3cb988c56b7ffa16ccc6b43f194b575a7a1e1e356f56c885d88886297cddf808
BLAKE2b-256 checksum
How to use checksums
ac25489abacc94fdb2bdc7e81fbdcf0d0c44b0a2404bb0c64b0a570690536e03
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.12

Release files / esclab-0.0.7-py3-none-any.whl

Download URL esclab-0.0.7-py3-none-any.whl
Size 90.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8c4d1e89401a0cce20fc71324b1608d129ebdf337356b0efca3381dab3ad3d44
BLAKE2b-256 checksum
How to use checksums
b5cb21eccabf84c03726a3906584d837a9694ffc370cda2af73acd914f673cd3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.12

Security for esclab 0.0.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page