Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avi/initial plus sp1 general #26

Merged
merged 13 commits into from
Dec 3, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
*.jl.*.cov
*.jl.cov
*.jl.mem
Manifest.toml
/Manifest.toml
/tmp
/.vscode
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
31 changes: 31 additions & 0 deletions Examples/Colberg_Morari_1990/ColbergMorari.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Workflow using XLSX input:
# 1. Import necessary packages:
using CompHENS
using Plots
using JuMP
using HiGHS

# 2. Specify path to xlsx file
file_path_xlsx = joinpath(@__DIR__, "CompHENS_interface_ColbergMorari.xlsx")

# 3. Construct the appropriate kind of problem: Here it is a `ClassicHENSProblem`
prob = ClassicHENSProblem(file_path_xlsx; ΔT_min = 20.0)

# 4. Subdivide into intervals and attain the hot and cold composite curves.
intervals = CompHENS.generate_heat_cascade_intervals(prob)
hot_ref_enthalpy, cold_ref_enthalpy = 0.0, 172.596
sorted_intervals = intervals
ylabel = "T [°C or K]"
xlabel = "Heat duty Q"

plt = CompHENS.plot_composite_curve(sorted_intervals; hot_ref_enthalpy, cold_ref_enthalpy, ylabel = "T [°C or K]", xlabel = "Heat duty Q")
ylims!((300,700))

# 5. Solve subproblem 1: minimum utilities.

# Using formulation of Prob. 16.5 Biegler, Grossmann, Westerberg book. Pg. 533.

min_utils = solve_minimum_utilities_subproblem(prob)



Binary file not shown.
2 changes: 0 additions & 2 deletions Manifest.toml

This file was deleted.

10 changes: 10 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ uuid = "873a7a81-90fd-4468-8feb-46a7ba58f008"
authors = ["Avinash Subramanian"]
version = "0.1.0"

[deps]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
GAMS = "1ca51c6a-1b4d-4546-9ae1-53e0a243ab12"
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
Kwonly = "18d08c8c-0732-55ee-a446-91a51d7b4206"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0"

[compat]
julia = "1.6"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure you wanna go with Julia 1.6? Is that necessary?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will update at a later stage. Would rather be conservative on this.


Expand Down
71 changes: 70 additions & 1 deletion src/CompHENS.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,74 @@
module CompHENS

# Write your package code here.
using DocStringExtensions
using Kwonly


"""
$(TYPEDEF)

Umbrella for all problems where `CompHENS.jl` is relevant
"""
abstract type AbstractSynthesisProblem end

"""
$(TYPEDEF)

The classical Heat Exchanger Network Synthesis (HENS) problem.
"""
abstract type AbstractHENSProblem <: AbstractSynthesisProblem end

"""
$(TYPEDEF)

Subproblem formulated while solving an `AbstractSynthesisProblem`
"""
abstract type AbstractSubProblem end


"""
$(TYPEDEF)

Type for technique to solve one or more `AbstractSynthesisProblem` types.
"""
abstract type AbstractSynthesisAlgorithm end

"""
$(TYPEDEF)

Algorithm to solve an `AbstractSubProblem`
"""
abstract type AbstractSubProblemAlgorithm end

"""
$(TYPEDEF)

Holds the solution of an `AbstractSynthesisProblem`
"""
abstract type AbstractSolution end

"""
$(TYPEDEF)

Holds the solution of an `AbstractSubProblem`
"""
abstract type AbstractSubProblemSolution end

const smallest_value = 1e-4

# Holds structures for streams
export AbstractStream, HotStream, ColdStream, AbstractUtility, SimpleHotUtility, SimpleColdUtility
include("Streams/streams.jl")

# Hold structures of problem types
export ClassicHENSProblem
include("ProblemConstructors/classic_hens_prob.jl")

# Holds all kinds of temperature intervals
export TemperatureInterval, generate_heat_cascade_intervals, plot_hot_composite_curve, plot_cold_composite_curve, plot_composite_curve
include("TemperatureIntervals/temperature_intervals.jl")

export solve_minimum_utilities_subproblem
include("SubProblems/minimum_utilities_subprob.jl")

end
61 changes: 61 additions & 0 deletions src/ProblemConstructors/classic_hens_prob.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using XLSX
using DataFrames
"""
$(TYPEDEF)
$(TYPEDFIELDS)

Holds the classical HENS problem with fixed stream data.
"""
mutable struct ClassicHENSProblem <: AbstractSynthesisProblem
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to be mutable? Since it holds the dictionaries already it feels like there is little value? Perhaps one wants to adjust $\Delta T_{min}$ after the fact?

Copy link
Owner Author

@avinashresearch1 avinashresearch1 Dec 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, made it immutable

hot_streams_dict::Dict{String, HotStream}
cold_streams_dict::Dict{String, ColdStream}
hot_utilities_dict::Dict{String, SimpleHotUtility}
cold_utilities_dict::Dict{String, SimpleColdUtility}
ΔT_min::Float64
@add_kwonly function ClassicHENSProblem(hot_streams_dict, cold_streams_dict, hot_utilities_dict = Dict{String, SimpleHotUtility}(), cold_utilities_dict = Dict{String, SimpleColdUtility}(); ΔT_min = 10)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have a good reason to make this an inner constructor? Usually inner constructors are used if you want to force some sort of sanity check that cannot be sidestepped. doesn't seem to be the case here. In that case it just reduces the extensibility but adds no value otherwise. Not sure if really critical here, though.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Difficult choice. I am inclined to keep the inner constructor as I may need to have these sanity checks as this constructor directly parses user information. Also, in the worst case if I find it hinders extensibility, its easier to delete than add it.

new(hot_streams_dict, cold_streams_dict, hot_utilities_dict, cold_utilities_dict, ΔT_min)
end
end

"""
$(TYPEDSIGNATURES)

Reads data from an XSLX file in `file_path_xlsx` and constructs a `ClassicHENSProblem`.

- **`file_path_xlsx`** needs to be a string that ends in .xlsx
"""
function ClassicHENSProblem(file_path_xlsx::String; ΔT_min)
# Column names of XLSX interface:
sheet_label, stream_label, type_label, t_in_label, t_out_label, heat_cap_label, heat_coeff_label, cost_label, forbidden_label, compulsory_label = "StreamData", "Stream", "Type [H, C, HU or CU]", "Supply Temperature T_in [C or K]", "Target Temperature T_out [C or K]", "Heat Capacity mCp [kW/C or kW/K]", "Heat transfer coefficient h [kW/m2C or kW/m2K]", "Cost [\$/kW]", "Forbidden Matches", "Compulsory Matches"
additional_user_fields = Set{String}(["Cost [\$/kW]", "Forbidden Matches", "Compulsory Matches", "Maximum Temperature", "Mininum Temperature"])

hot_streams_dict = Dict{String, HotStream}()
cold_streams_dict = Dict{String, ColdStream}()
hot_utilities_dict = Dict{String, SimpleHotUtility}()
cold_utilities_dict = Dict{String, SimpleColdUtility}()

stream_data_df = XLSX.openxlsx(file_path_xlsx) do xf
sheet_label in XLSX.sheetnames(xf) || error("Sheet with name `$(sheet_label)` not found. ")
DataFrame(XLSX.gettable(xf[sheet_label]; infer_eltypes=true))
end

names_stream_df = names(stream_data_df)

for row in eachrow(stream_data_df)
# add_user_data field in all stream types.
add_user_data = Dict{String, Any}(k => row[k] for k in additional_user_fields if (k in names_stream_df && !ismissing(row[k])))
if row[type_label] == "H" # Hot stream
hot_streams_dict[row[stream_label]] = HotStream(row[stream_label], row[t_in_label], row[t_out_label], row[heat_cap_label], row[heat_coeff_label], add_user_data)
elseif row[type_label] == "C" # Cold stream
cold_streams_dict[row[stream_label]] = ColdStream(row[stream_label], row[t_in_label], row[t_out_label], row[heat_cap_label], row[heat_coeff_label], add_user_data)
elseif row[type_label] == "HU" # Hot utility stream
hot_utilities_dict[row[stream_label]] = SimpleHotUtility(row[stream_label], row[t_in_label], row[t_out_label], row[heat_coeff_label], add_user_data)
elseif row[type_label] == "CU" # Cold utility stream
cold_utilities_dict[row[stream_label]] = SimpleColdUtility(row[stream_label], row[t_in_label], row[t_out_label], row[heat_coeff_label], add_user_data)
end
end

## TODO: Logic to get `ΔT_min` from XLSX sheet.
return ClassicHENSProblem(hot_streams_dict, cold_streams_dict, hot_utilities_dict, cold_utilities_dict; ΔT_min)
end

89 changes: 89 additions & 0 deletions src/Streams/streams.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
abstract type AbstractStream end


"""
$(TYPEDEF)
$(TYPEDFIELDS)

Single hot stream
"""
mutable struct HotStream <: AbstractStream
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the mutability of this I like. I could see adjusting stream data to be an important use case even after the problem has been defined.

name::String # May eventually need parametric types when these can be variables.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps prefer the use of symbols here? There is no huge difference. If you end up comparing stream names a lot then Symbols are faster. I doubt this is gonna be critical here. Perhaps actually a little relevant is that I think they are easier to type - : instead of " ". If you manipulate the stream names a lot (which I doubt?) then Strings should be preferred.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good qn. The major reason to use strings is once again this is constructed from the users XLSX sheet, and the user may want to give long names with spaces e.g., "Reactor effluent hot stream". Easier with strings than symbols.

T_in::Float64
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it necessary to restrict to Float64? What if someone uses Float32 for whatever reason or inputs integer data for temperatures. Maybe make the type parametric, and handle promotion to the same number type for all inputs?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point to use parametric types. #28

I do accept any type in the constructor and promote to Float64 as seen in the constructor code.

T_out::Float64
mcp::Float64
h::Float64 # Film heat transfer coefficient
add_user_data::Dict{String, Any} # Used to hold useful information moving forwards. `Any` here may ruin type inference?
calc::Dict{String, Float64} # New data calculated from input data
@add_kwonly function HotStream(name, T_in, T_out, mcp, h, add_user_data = Dict{String, Any}(), calc = Dict{String, Float64}())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same issue regarding the inner constructor as before.

T_in isa Real && T_out isa Real && mcp isa Real && h isa Real || error("Input data contains a non-real number")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use dispatch for that?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, this I don't understand. The above is a sanity check of user data. I want to error if the user gives in garbage in the XLSX sheet.

T_in >= T_out || error("Supply and Target temperature don't match stream type")
mcp > smallest_value && h > smallest_value || error("mcp or h values infeasible")
new(name, Float64(T_in), Float64(T_out), Float64(mcp), Float64(h), add_user_data, calc)
end
end


"""
$(TYPEDEF)
$(TYPEDFIELDS)

Single cold stream
"""
mutable struct ColdStream <: AbstractStream
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see HotStream comments

name::String
T_in::Float64
T_out::Float64
mcp::Float64
h::Float64
add_user_data::Dict{String, Any}
calc::Dict{String, Float64}
@add_kwonly function ColdStream(name, T_in, T_out, mcp, h, add_user_data = Dict{String, Any}(), calc = Dict{String, Float64}())
T_in isa Real && T_out isa Real && mcp isa Real && h isa Real || error("Input data contains a non-real number")
T_in <= T_out || error("Supply and Target temperature don't match stream type")
mcp > smallest_value && h > smallest_value || error("mcp or h values infeasible")
new(name, Float64(T_in), Float64(T_out), Float64(mcp), Float64(h), add_user_data, calc)
end
end

abstract type AbstractUtility end

"""
$(TYPEDEF)
$(TYPEDFIELDS)

Simple hot utility stream. Single stream, no configuration information.
"""
mutable struct SimpleHotUtility <: AbstractUtility
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comments for other ColdStream

name::String
T_in::Float64
T_out::Float64
h::Float64
add_user_data::Dict{String, Any}
calc::Dict{String, Float64}
@add_kwonly function SimpleHotUtility(name, T_in, T_out, h, add_user_data = Dict{String, Any}(), calc = Dict{String, Float64}())
T_in isa Real && T_out isa Real && h isa Real || error("Input data contains a non-real number")
h > smallest_value || error("h value infeasible")
new(name, Float64(T_in), Float64(T_out), Float64(h), add_user_data, calc)
end
end

"""
$(TYPEDEF)
$(TYPEDFIELDS)

Simple cold utility stream. Single stream, no configuration information.
"""
mutable struct SimpleColdUtility <: AbstractUtility
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see SimpleHotUtility comments.

name::String
T_in::Float64
T_out::Float64
h::Float64 #
add_user_data::Dict{String, Any}
calc::Dict{String, Float64}
@add_kwonly function SimpleColdUtility(name, T_in, T_out, h, add_user_data = Dict{String, Any}(), calc = Dict{String, Float64}())
T_in isa Real && T_out isa Real && h isa Real || error("Input data contains a non-real number")
h > smallest_value || error("h value infeasible")
new(name, Float64(T_in), Float64(T_out), Float64(h), add_user_data, calc)
end
end
55 changes: 55 additions & 0 deletions src/SubProblems/minimum_utilities_subprob.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using JuMP
using HiGHS

"""
$(TYPEDSIGNATURES)

Constructs and solves a minimum utilities optimization problem.
Returns:
- sol: A dictionary mapping the `keys(prob.hot_utilities_dict)`, `keys(prob.cold_utilities_dict)` to their minimum utility requirements.
Supports multiple utilities if appropriately matched to interval.
[TODO: Utility costs]
"""
function solve_minimum_utilities_subproblem(prob::ClassicHENSProblem; time_limit = 60.0, presolve = true, optimizer = HiGHS.Optimizer, verbose = true)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of macros to create the problem is very slow. Avoiding that is a lot of work and takes a lot of learning about JuMP (or ideally MathOptInterface) though. Depending on how big the problems become, the inefficiency of macro usage can actually become very noticeable (especially when you start thinking about the nonlinear problems). For now I think this is fine but something to keep in mind.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, will make an issue #27

@info "Solving the minimum utilities subproblem"
intervals = CompHENS.generate_heat_cascade_intervals(prob)
subprob = Model()
HU_set = keys(prob.hot_utilities_dict)
CU_set = keys(prob.cold_utilities_dict)

@variable(subprob, 0 <= Q_in[HU_set])
@variable(subprob, 0 <= Q_out[CU_set])
@variable(subprob, 0 <= R[intervals]) # Notation: R[interval] is the residual heat exiting a given interval
JuMP.fix(R[last(intervals)], 0.0; force = true)

# First interval: Entering == Leaving
@constraint(subprob,
sum(Q_in[hu] for hu in keys(first(intervals).hot_utilities_contribs)) + first(intervals).total_stream_heat_in == R[first(intervals)] + sum(Q_out[cu] for cu in keys(first(intervals).cold_utilities_contribs)) + first(intervals).total_stream_heat_out)

# Remaining intervals
@constraint(subprob, [i in 2:length(intervals)],
R[intervals[i-1]] + sum(Q_in[hu] for hu in keys(intervals[i].hot_utilities_contribs)) + intervals[i].total_stream_heat_in == R[intervals[i]] + sum(Q_out[cu] for cu in keys(intervals[i].cold_utilities_contribs)) + intervals[i].total_stream_heat_out)

# Objective: TODO: Add utility costs.
@objective(subprob, Min, sum(Q_in) + sum(Q_out))
set_optimizer(subprob, optimizer)
presolve && set_optimizer_attribute(subprob, "presolve", "on")
set_optimizer_attribute(subprob, "time_limit", time_limit)
optimize!(subprob)
if verbose
@show termination_status(subprob)
@show primal_status(subprob)
@show dual_status(subprob)
end


# Post-processing
sol = Dict()
for hu in HU_set
push!(sol, hu => value.(Q_in[hu]))
end
for cu in CU_set
push!(sol, cu => value.(Q_out[cu]))
end
return sol
end
Loading