Overview¶
CALVIN is a network flow optimization model of California’s water supply system. It uses Pyomo to formulate linear programs over a node-link network and solves them with LP/MIP solvers (HiGHS, CBC, Gurobi, CPLEX).
Installation¶
Clone the repo and install into your environment. The install step is required so that
calvin is importable from any working directory (including scripts/).
pip:
git clone https://github.com/wyattarnold/calvin.git
cd calvin
pip install -e ".[solver]"
conda:
git clone https://github.com/wyattarnold/calvin.git
cd calvin
conda create -n calvin python=3.11
conda activate calvin
pip install -e ".[solver]"
pip works inside conda environments and will install all dependencies. The -e
flag installs the package in editable mode so that source changes take effect immediately.
To run the web app locally, use the app extra instead of (or in addition to) solver:
pip install -e ".[app]"
The model can be run in three modes:
Perfect foresight — a single large LP over the full time horizon (e.g. 82 water years), solved once.
Annual (constraint-based) — a sequence of single-year LPs where end-of-period storage is managed by imposing minimum storage constraints as a fraction of reservoir capacity. No economic penalties are used.
Annual (COSVF + evolutionary) — a sequence of single-year LPs connected by Carryover Storage Value Functions (COSVFs) that penalize end-of-year reservoir storage to approximate the value of water carried into the next year. Penalty parameters are optimized via an evolutionary algorithm.
Perfect Foresight Mode¶
In perfect foresight mode, the CALVIN class loads a single links CSV containing the
full time-expanded network and solves it directly:
from calvin import CALVIN, postprocess
calvin = CALVIN('links82yr.csv')
calvin.create_pyomo_model(debug_mode=True, debug_cost=2e10)
calvin.solve_pyomo_model(solver='highs', nproc=1, debug_mode=True)
calvin.create_pyomo_model(debug_mode=False)
calvin.solve_pyomo_model(solver='highs', nproc=1, debug_mode=False)
postprocess(calvin.df, calvin.model, resultdir='results')
Annual Mode (Constraint-Based)¶
The simplest limited-foresight approach solves one water year at a time without economic
storage penalties. Instead, the eop_constraint_multiplier method sets the lower bound on
end-of-September reservoir storage to a fraction of each reservoir’s capacity. This prevents
the optimizer from completely emptying reservoirs within a single year.
The method uses SR_stats.csv (loaded automatically by CALVIN) which contains
min and max storage for each surface reservoir. For a given fraction x, the
end-of-period lower bound for reservoir k is set to:
where \(S_{min,k}\) and \(S_{max,k}\) are the minimum and maximum storage from
SR_stats.csv.
The annual loop requires one links CSV file per water year (e.g. exported from
calvin.network). End-of-period storage from each year is passed as initial conditions
to the next via the ic parameter:
from calvin import CALVIN, postprocess
eop = None
for wy in range(1922, 2004):
print(f'\nNow running WY {wy}')
calvin = CALVIN(f'calvin/data/annual/linksWY{wy}.csv', ic=eop)
calvin.eop_constraint_multiplier(0.1)
calvin.create_pyomo_model(debug_mode=True, debug_cost=2e8)
calvin.solve_pyomo_model(solver='highs', nproc=1, debug_mode=True, maxiter=15)
calvin.create_pyomo_model(debug_mode=False)
calvin.solve_pyomo_model(solver='highs', nproc=1, debug_mode=False)
# postprocess appends to per-year result directories; returns EOP storage for next year
eop = postprocess(calvin.df, calvin.model,
resultdir=f'results/annual/WY{wy}', annual=True)
Note
The constraint fraction (here 0.1, i.e. 10% of capacity) is a tunable parameter.
Lower values give the optimizer more freedom but risk over-drafting storage; higher
values are more conservative.
Combining Annual Results¶
After the annual loop completes, use calvin.postprocessor.combine_annual_results()
to concatenate the per-year CSV files into single timeseries files:
from calvin import combine_annual_results
combine_annual_results(
years=range(1922, 2004),
annual_dir='results/annual',
output_dir='results',
)
This reads results/annual/WY{year}/*.csv for each year and writes concatenated files
to results/.
Annual Mode (COSVF + Evolutionary)¶
The COSVF approach also solves the network one water year at a time, but replaces the simple storage constraints with economic penalty functions on end-of-period storage. These Carryover Storage Value Functions represent the marginal value of storing water for future use.
Two penalty types are supported:
Type 1 (quadratic): for surface reservoirs — defined by \(P_{min}\) and \(P_{max}\) parameters that shape a quadratic penalty curve between minimum operating storage and full carryover capacity. The curve is linearized into piecewise segments for the LP.
Type 2 (linear): for groundwater reservoirs — a single marginal penalty \(P_{GW}\) applied to storage below the initial level.
Evolutionary Optimization of COSVF Parameters¶
The penalty parameters are not known a priori. The cosvfea module uses the
NSGA-III multi-objective evolutionary algorithm (via DEAP)
to search for optimal penalty values. The three objective functions minimized are:
Shortage + operational costs (\$/year) — total annualized cost across all demand and operational links.
Groundwater overdraft (MAF/year) — net depletion across all groundwater basins.
Mean penalty magnitude — regularization to avoid unnecessarily large penalties.
Each candidate solution (individual) is a vector of penalty parameters for all reservoirs.
The EA evaluates each individual by running the full annual COSVF sequence and computing
the three fitness values. The search is designed to run in parallel using mpi4py.
# main-cosvfea.py (simplified)
from calvin import cosvfea
def cosvf_evaluate(pcosvf):
calvin = cosvfea.COSVF(pwd='./my-models/calvin-cosvf')
calvin.create_pyomo_model(debug_mode=True)
return calvin.cosvf_solve(solver='cbc', nproc=1, pcosvf=pcosvf)
toolbox = cosvfea.cosvf_ea_toolbox(
cosvf_evaluate=cosvf_evaluate,
nrtype=[26, 32], # 26 quadratic, 32 linear reservoirs
mu=95
)
Running with MPI:
mpirun -n <ncpus> python main-cosvfea.py
COSVF Ending-Storage Constraint¶
prepare_cosvf calls build_matrix() with constrain_ending='all', which
sets lb = ub = final_val on every storage node’s → FINAL link. This
fixes ending storage in the template water-year LP used to build the
single-step network structure. However, in an actual COSVF run the ending-storage
constraint is not enforced this way. The COSVF solver replaces the → FINAL
link bounds with the piecewise-linear penalty curve for each reservoir: ending
storage is a free decision variable penalised by the COSVF objective term, not pinned
to a fixed target.
Preparing COSVF Input Data¶
Before running a COSVF model, generate the required input files using
calvin.network.prepare.prepare_cosvf(). This reads the
calvin-network-data repository
directly (no prior perfect-foresight run required) and writes five files into the
specified output directory:
python -m calvin.network.cli prepare-cosvf \
--data ../calvin-network-data/data \
--output ./my-models/calvin-cosvf
Or equivalently from Python:
from calvin.network import prepare_cosvf
prepare_cosvf(data_path='../calvin-network-data/data',
output_dir='./my-models/calvin-cosvf')
The generated files are:
links.csvNetwork for a single water year (WY 1922). This serves as the template LP that is re-solved for each year in sequence with updated inflows and inital storage conditions.
Schema:
i,j,k,cost,amplitude,lower_bound,upper_boundcosvf-params.csvPenalty parameters for each reservoir. Contains columns
r,param,valuewhere:For Type 2 (groundwater) reservoirs: a single row with
param=pand the linear penalty value.For Type 1 (surface) reservoirs: two rows per reservoir with
param=pminandparam=pmaxdefining the quadratic curve endpoints.
Note
The default values are placeholders (e.g.
-100.0for all groundwater). They are meant to be replaced by the evolutionary optimization.r-dict.jsonDictionary of reservoirs keyed by node ID (e.g.
SR_SHA,GW_01). Each entry defines:eop_init: target end-of-period storage level (TAF). Prefersendingstoragefrom the network data; falls back toinitialstorageif no ending storage is defined.lb: minimum end-of-September storage (TAF)ub: maximum carryover capacity (TAF)type:0(no penalty),1(quadratic), or2(linear)cosvf_param_index: row index intocosvf-params.csv(zero-indexed)k_count: number of piecewise segments for the penalty curve
Important
All GW nodes must precede all SR nodes in
r-dict.json. Within each group the ordering is: type-2 (sorted alphabetically), then type-0 (sorted); followed by type-1 (sorted), then type-0 (sorted). The EA parameter vector is laid out as[gw_type2_params..., sr_pmin_0, sr_pmax_0, ...]andcosvf_check_bounds(rtype1_start_idx)relies on the index where SR parameters begin. If GW and SR entries are interleaved the index is wrong and the EA malfunctions.Note
Only
GW_HFandGW_KRNare classified as type 0 (no COSVF penalty) because pumping links are also constrained to zero (UBC = 0), and so they are inactive. All other GW basins — including the Southern California basins (GW_AV,GW_CH,GW_EW,GW_IM,GW_MJ,GW_MWD,GW_OW,GW_SBV,GW_SC,GW_SD,GW_VC) and the Central Valley basins (GW_01–GW_21) — are type 2.inflows.csvMonthly external inflows for the full period of analysis.
Schema:
date,j,flow_tafvariable-constraints.csvLinks with upper/lower bounds that change across water years (e.g. seasonal environmental flow requirements). Identified directly from timeseries bound types (
LBT,UBT,EQT) in the network data.Schema:
date,i,j,k,lower_bound,upper_boundImportant
Rows are emitted for all piecewise segments (
k = 0 … N-1), not justk=0. Each segment’s bounds are proportional to its share of the total physical capacity (resolved via_resolve_costs/_reconcile_step_cost). Sinks and storage self-links are the exception — they only havek=0.
Web App¶
CALVIN includes a FastAPI + React web app for interactively exploring the network and optimization results. See the Web App page for full documentation.
A hosted version is available at calvin-network-app.onrender.com.
To run locally:
pip install "calvin[app]"
python -m calvin.app serve --data ../calvin-network-data/data --local