import fine as fn
import numpy as np
import pandas as pd
%load_ext autoreload
%autoreload 2
Water supply of a small mountain village¶
Two new houses (house 5 and 6) were built in a small mountain village in Utopia which requires an update of the existing clean water supply system of the village which now consists of 6 houses:

Water demand¶
The demand for clean water occurs in spring between 5 am and 11 pm, in summer between 4 am and 12 pm, in autumn between 5 am and 11 pm and in winter between 6 am and 11 pm. The demand for one house assumes random values between 0 to 1 Uh (Unit*hour) during the demand hours. These values are uniformly distributed and are 0 outside the demand hours.
Water supply¶
The water supply comes from a small tributary of a glacier river, which provides more water in summer and less in winter: the profile is given for each hour of the year as
f(t) = 8 * sin(π*t/8760) + g(t)
where g(t) is a uniformly distributed random value between 0 and 4.
Water storage¶
Clean water can be stored in a water tank (newly purchased). The invest per capacity is 100€/Uh, the economic lifetime is 20 years.
Water treatment¶
The river water is converted to clean water in a water treatment plant (newly purchased). The invest per capacity is 7000€/U, the economic lifetime is 20 years. Further, it needs some electricity wherefore it has operational cost of 0.05 €/U.
Water transmission¶
The clean water can be transported via water pipes, where some already exist between the houses 1-4, the water treatment plant and the water tank, however new ones might need to be built to connect the newly built houses or reinforce the transmission along the old pipes. The invest for new pipes per capacity is 100 €/(m*U), the invest if a new pipe route is built is 500 €/(m*U), the economic lifetime is 20 years.
locations = [
"House 1",
"House 2",
"House 3",
"House 4",
"House 5",
"House 6",
"Node 1",
"Node 2",
"Node 3",
"Node 4",
"Water treatment",
"Water tank",
]
commodityUnitDict = {"clean water": "U", "river water": "U"}
commodities = {"clean water", "river water"}
numberOfTimeSteps = 8760
hoursPerTimeStep = 1
esM = fn.EnergySystemModel(
locations=set(locations),
commodities=commodities,
numberOfTimeSteps=8760,
commodityUnitsDict=commodityUnitDict,
hoursPerTimeStep=1,
costUnit="1e3 Euro",
lengthUnit="m",
)
Source¶
riverFlow = pd.DataFrame(np.zeros((8760, 12)), columns=locations)
np.random.seed(42)
riverFlow.loc[:, "Water treatment"] = np.random.uniform(0, 4, (8760)) + 8 * np.sin(
np.pi * np.arange(8760) / 8760
)
esM.add(
fn.Source(
esM=esM,
name="River",
commodity="river water",
hasCapacityVariable=False,
operationRateMax=riverFlow,
opexPerOperation=0.05,
)
)
Conversion¶
eligibility = pd.Series([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], index=locations)
esM.add(
fn.Conversion(
esM=esM,
name="Water treatment plant",
physicalUnit="U",
commodityConversionFactors={"river water": -1, "clean water": 1},
hasCapacityVariable=True,
locationalEligibility=eligibility,
investPerCapacity=7,
opexPerCapacity=0.02 * 7,
interestRate=0.08,
economicLifetime=20,
)
)
Storage¶
eligibility = pd.Series([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], index=locations)
esM.add(
fn.Storage(
esM=esM,
name="Water tank",
commodity="clean water",
hasCapacityVariable=True,
chargeRate=1 / 24,
dischargeRate=1 / 24,
locationalEligibility=eligibility,
investPerCapacity=0.10,
opexPerCapacity=0.02 * 0.1,
interestRate=0.08,
economicLifetime=20,
)
)
Transmission¶
Distances between eligible regions¶
distances = np.array(
[
[0, 0, 0, 0, 0, 0, 0, 0, 38, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 38, 40, 0, 105, 0, 0, 0, 0],
[0, 0, 38, 40, 0, 0, 105, 0, 100, 0, 0, 0],
[38, 40, 0, 0, 0, 0, 0, 100, 0, 30, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 30, 0, 20, 50],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0],
]
)
distances = pd.DataFrame(distances, index=locations, columns=locations)
distances
| House 1 | House 2 | House 3 | House 4 | House 5 | House 6 | Node 1 | Node 2 | Node 3 | Node 4 | Water treatment | Water tank | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| House 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 38 | 0 | 0 | 0 |
| House 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 40 | 0 | 0 | 0 |
| House 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 38 | 0 | 0 | 0 | 0 |
| House 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 40 | 0 | 0 | 0 | 0 |
| House 5 | 0 | 0 | 0 | 0 | 0 | 0 | 38 | 0 | 0 | 0 | 0 | 0 |
| House 6 | 0 | 0 | 0 | 0 | 0 | 0 | 40 | 0 | 0 | 0 | 0 | 0 |
| Node 1 | 0 | 0 | 0 | 0 | 38 | 40 | 0 | 105 | 0 | 0 | 0 | 0 |
| Node 2 | 0 | 0 | 38 | 40 | 0 | 0 | 105 | 0 | 100 | 0 | 0 | 0 |
| Node 3 | 38 | 40 | 0 | 0 | 0 | 0 | 0 | 100 | 0 | 30 | 0 | 0 |
| Node 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 30 | 0 | 20 | 50 |
| Water treatment | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 20 | 0 | 0 |
| Water tank | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 50 | 0 | 0 |
Old water pipes¶
capacityFix = np.array(
[
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0],
[1, 1, 0, 0, 0, 0, 0, 2, 0, 4, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 4],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0],
]
)
capacityFix = pd.DataFrame(capacityFix, index=locations, columns=locations)
capacityFix
| House 1 | House 2 | House 3 | House 4 | House 5 | House 6 | Node 1 | Node 2 | Node 3 | Node 4 | Water treatment | Water tank | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| House 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
| House 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
| House 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| House 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| House 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| House 6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Node 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Node 2 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 2 | 0 | 0 | 0 |
| Node 3 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 2 | 0 | 4 | 0 | 0 |
| Node 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 4 | 0 | 4 | 4 |
| Water treatment | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 4 | 0 | 0 |
| Water tank | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 4 | 0 | 0 |
The old pipes have many leckages wherefore they lose 0.1%/m of the water they transport.
isBuiltFix = capacityFix.copy()
isBuiltFix[isBuiltFix > 0] = 1
esM.add(
fn.Transmission(
esM=esM,
name="Old water pipes",
commodity="clean water",
losses=0.1e-2,
distances=distances,
hasCapacityVariable=True,
hasIsBuiltBinaryVariable=True,
bigM=100,
capacityFix=capacityFix,
isBuiltFix=isBuiltFix,
)
)
New water pipes¶
incidence = np.array(
[
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0],
[1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
]
)
eligibility = pd.DataFrame(incidence, index=locations, columns=locations)
eligibility
| House 1 | House 2 | House 3 | House 4 | House 5 | House 6 | Node 1 | Node 2 | Node 3 | Node 4 | Water treatment | Water tank | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| House 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
| House 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
| House 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| House 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| House 5 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| House 6 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| Node 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 0 |
| Node 2 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 |
| Node 3 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
| Node 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
| Water treatment | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
| Water tank | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
The new are pipes are better but still lose 0.05%/m of the water they transport.
esM.add(
fn.Transmission(
esM=esM,
name="New water pipes",
commodity="clean water",
losses=0.05e-2,
distances=distances,
hasCapacityVariable=True,
hasIsBuiltBinaryVariable=True,
bigM=100,
locationalEligibility=eligibility,
investPerCapacity=0.1,
investIfBuilt=0.5,
interestRate=0.08,
economicLifetime=50,
)
)
Sink¶
winterHours = np.append(range(8520, 8760), range(1920))
springHours, summerHours, autumnHours = (
np.arange(1920, 4128),
np.arange(4128, 6384),
np.arange(6384, 8520),
)
demand = pd.DataFrame(np.zeros((8760, 12)), columns=list(locations))
np.random.seed(42)
demand[locations[0:6]] = np.random.uniform(0, 1, (8760, 6))
demand.loc[winterHours[(winterHours % 24 < 5) | (winterHours % 24 >= 23)]] = 0
demand.loc[springHours[(springHours % 24 < 4)]] = 0
demand.loc[summerHours[(summerHours % 24 < 5) | (summerHours % 24 >= 23)]] = 0
demand.loc[autumnHours[(autumnHours % 24 < 6) | (autumnHours % 24 >= 23)]] = 0
np.float64(demand.sum().sum())
np.float64(19955.415289775883)
esM.add(
fn.Sink(
esM=esM,
name="Water demand",
commodity="clean water",
hasCapacityVariable=False,
operationRateFix=demand,
)
)
Optimize the system¶
esM.aggregateTemporally(numberOfTypicalPeriods=7)
Clustering time series data with 7 typical periods and 24 time steps per period further clustered to 12 segments per period... (6.9524 sec)
# esM.optimize(timeSeriesAggregation=True, optimizationSpecs='LogToConsole=1 OptimalityTol=1e-6 crossover=1')
esM.optimize(
timeSeriesAggregation=True,
solver=fn.utils.ImplementedSolvers.STANDARD_SOLVER.value,
)
Time series aggregation specifications: Number of typical periods:7, number of time steps per period:24, number of segments per period:12 Declaring sets, variables and constraints for SourceSinkModel declaring sets... declaring variables... declaring constraints... (0.0352 sec) Declaring sets, variables and constraints for ConversionModel declaring sets... declaring variables... declaring constraints... (0.0110 sec) Declaring sets, variables and constraints for StorageModel declaring sets... declaring variables... declaring constraints... (0.1011 sec) Declaring sets, variables and constraints for TransmissionModel declaring sets... declaring variables... declaring constraints... (0.0749 sec) Declaring shared potential constraint... (0.0013 sec) Declaring linked component quantity constraint... (0.0000 sec) Declaring commodity balances... (0.0485 sec) (0.0000 sec) Declaring objective function... (0.1560 sec) WARNING: Could not locate the 'glpsol' executable, which is required for solver 'glpk'
--------------------------------------------------------------------------- ApplicationError Traceback (most recent call last) Cell In[17], line 2 1 # esM.optimize(timeSeriesAggregation=True, optimizationSpecs='LogToConsole=1 OptimalityTol=1e-6 crossover=1') ----> 2 esM.optimize(timeSeriesAggregation=True, solver="glpk") File C:\Programming\FINE\fine\energySystemModel.py:2077, in EnergySystemModel.optimize(self, declaresOptimizationProblem, relaxIsBuiltBinary, timeSeriesAggregation, logFileName, threads, solver, timeLimit, optimizationSpecs, warmstart, relevanceThreshold, includePerformanceSummary) 2075 elif solver == "glpk": 2076 optimizer.set_options(optimizationSpecs) -> 2077 solver_info = optimizer.solve(self.pyM, tee=True) 2078 else: 2079 solver_info = optimizer.solve(self.pyM, tee=True) File c:\Users\j.belina\AppData\Local\miniforge3\envs\fine_22_09_25\Lib\site-packages\pyomo\opt\base\solvers.py:560, in OptSolver.solve(self, *args, **kwds) 557 def solve(self, *args, **kwds): 558 """Solve the problem""" --> 560 self.available(exception_flag=True) 561 # 562 # If the inputs are models, then validate that they have been 563 # constructed! Collect suffix names to try and import from solution. 564 # 565 from pyomo.core.base.block import BlockData File c:\Users\j.belina\AppData\Local\miniforge3\envs\fine_22_09_25\Lib\site-packages\pyomo\opt\solver\shellcmd.py:140, in SystemCallSolver.available(self, exception_flag) 138 if exception_flag: 139 msg = "No executable found for solver '%s'" --> 140 raise ApplicationError(msg % self.name) 141 return False 142 return True ApplicationError: No executable found for solver 'glpk'
Selected results output¶
Sources and Sinks¶
esM.getOptimizationSummary("SourceSinkModel", outputLevel=2)
Storage¶
esM.getOptimizationSummary("StorageModel", outputLevel=2)
Conversion¶
esM.getOptimizationSummary("ConversionModel", outputLevel=2)
Transmission¶
esM.getOptimizationSummary("TransmissionModel", outputLevel=2)
esM.componentModelingDict["TransmissionModel"].operationVariablesOptimum.sum(axis=1)