mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 00:08:52 -06:00
web: Initial backend implementation
This commit is contained in:
1
web/backend/.gitignore
vendored
Normal file
1
web/backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
jobs
|
||||||
17
web/backend/Project.toml
Normal file
17
web/backend/Project.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name = "Backend"
|
||||||
|
uuid = "948642ed-e3f9-4642-9296-0f1eaf40c938"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Alinson S. Xavier <git@axavier.org>"]
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
|
||||||
|
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
|
||||||
|
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||||
|
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||||
|
UnitCommitment = "64606440-39ea-11e9-0f29-3303a1d3d877"
|
||||||
|
|
||||||
|
[compat]
|
||||||
|
CodecZlib = "0.7.8"
|
||||||
|
HTTP = "1.10.19"
|
||||||
|
JSON = "0.21.4"
|
||||||
|
Random = "1.11.0"
|
||||||
67
web/backend/src/Backend.jl
Normal file
67
web/backend/src/Backend.jl
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2025, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
module Backend
|
||||||
|
|
||||||
|
using HTTP
|
||||||
|
using Random
|
||||||
|
using JSON
|
||||||
|
using CodecZlib
|
||||||
|
using UnitCommitment
|
||||||
|
|
||||||
|
basedir = joinpath(dirname(@__FILE__), "..")
|
||||||
|
|
||||||
|
function submit(req; optimizer)
|
||||||
|
# Check if request body is empty
|
||||||
|
compressed_body = HTTP.payload(req)
|
||||||
|
if isempty(compressed_body)
|
||||||
|
return HTTP.Response(400, "Error: No file provided")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validate compressed JSON by decompressing and parsing
|
||||||
|
try
|
||||||
|
decompressed_data = transcode(GzipDecompressor, compressed_body)
|
||||||
|
JSON.parse(String(decompressed_data))
|
||||||
|
catch e
|
||||||
|
return HTTP.Response(400, "Error: Invalid compressed JSON")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate random job ID (lowercase letters and numbers)
|
||||||
|
job_id = randstring(['a':'z'; '0':'9'], 16)
|
||||||
|
|
||||||
|
# Create job directory
|
||||||
|
job_dir = joinpath(basedir, "jobs", job_id)
|
||||||
|
mkpath(job_dir)
|
||||||
|
|
||||||
|
# Save input file
|
||||||
|
json_path = joinpath(job_dir, "input.json.gz")
|
||||||
|
write(json_path, compressed_body)
|
||||||
|
|
||||||
|
# Optimize file
|
||||||
|
instance = UnitCommitment.read(json_path)
|
||||||
|
model = UnitCommitment.build_model(; instance, optimizer)
|
||||||
|
UnitCommitment.optimize!(model)
|
||||||
|
solution = UnitCommitment.solution(model)
|
||||||
|
UnitCommitment.write("$job_dir/output.json", solution)
|
||||||
|
|
||||||
|
# Return job ID as JSON
|
||||||
|
response_body = JSON.json(Dict("job_id" => job_id))
|
||||||
|
return HTTP.Response(200, response_body)
|
||||||
|
end
|
||||||
|
|
||||||
|
function jobs_view(req)
|
||||||
|
return HTTP.Response(200, "OK")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function start_server(port::Int = 8080; optimizer)
|
||||||
|
Random.seed!()
|
||||||
|
router = HTTP.Router()
|
||||||
|
HTTP.register!(router, "POST", "/submit", req -> submit(req; optimizer))
|
||||||
|
HTTP.register!(router, "GET", "/jobs/*/view", jobs_view)
|
||||||
|
server = HTTP.serve!(router, port; verbose = false)
|
||||||
|
return server
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
23
web/backend/test/Project.toml
Normal file
23
web/backend/test/Project.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "BackendT"
|
||||||
|
uuid = "27da795e-16fd-43bd-a2ba-f77bdecaf977"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Alinson S. Xavier <git@axavier.org>"]
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
Backend = "948642ed-e3f9-4642-9296-0f1eaf40c938"
|
||||||
|
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
|
||||||
|
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
|
||||||
|
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
|
||||||
|
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||||
|
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
|
||||||
|
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||||
|
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||||
|
|
||||||
|
[compat]
|
||||||
|
CodecZlib = "0.7.8"
|
||||||
|
HTTP = "1.10.19"
|
||||||
|
HiGHS = "1.20.1"
|
||||||
|
JSON = "0.21.4"
|
||||||
|
JuliaFormatter = "2.2.0"
|
||||||
|
Revise = "3.12.0"
|
||||||
|
Test = "1.11.0"
|
||||||
87
web/backend/test/src/BackendT.jl
Normal file
87
web/backend/test/src/BackendT.jl
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2025, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
module BackendT
|
||||||
|
|
||||||
|
using Test
|
||||||
|
using HTTP
|
||||||
|
using JSON
|
||||||
|
using CodecZlib
|
||||||
|
import Backend
|
||||||
|
import JuliaFormatter
|
||||||
|
using HiGHS
|
||||||
|
|
||||||
|
basedir = dirname(@__FILE__)
|
||||||
|
port = 32617
|
||||||
|
|
||||||
|
function fixture(path::String)::String
|
||||||
|
return "$basedir/../fixtures/$path"
|
||||||
|
end
|
||||||
|
|
||||||
|
function with_server(f)
|
||||||
|
logger = Test.TestLogger()
|
||||||
|
# server = Base.CoreLogging.with_logger(logger) do
|
||||||
|
# Backend.start_server(port)
|
||||||
|
# end
|
||||||
|
server = Backend.start_server(port; optimizer=HiGHS.Optimizer)
|
||||||
|
try
|
||||||
|
f()
|
||||||
|
finally
|
||||||
|
close(server)
|
||||||
|
end
|
||||||
|
return filter!(x -> x.group == :access, logger.logs)
|
||||||
|
end
|
||||||
|
|
||||||
|
function test_usage()
|
||||||
|
with_server() do
|
||||||
|
# Read the compressed fixture file
|
||||||
|
compressed_data = read(fixture("case14.json.gz"))
|
||||||
|
|
||||||
|
# Submit test case
|
||||||
|
response = HTTP.post(
|
||||||
|
"http://localhost:$port/submit",
|
||||||
|
["Content-Type" => "application/gzip"],
|
||||||
|
compressed_data
|
||||||
|
)
|
||||||
|
@test response.status == 200
|
||||||
|
|
||||||
|
# Check response
|
||||||
|
response_data = JSON.parse(String(response.body))
|
||||||
|
@test haskey(response_data, "job_id")
|
||||||
|
job_id = response_data["job_id"]
|
||||||
|
@test length(job_id) == 16
|
||||||
|
@test all(c -> c in ['a':'z'; '0':'9'], collect(job_id))
|
||||||
|
|
||||||
|
# Verify the compressed file was saved correctly
|
||||||
|
job_dir = joinpath(Backend.basedir, "jobs", job_id)
|
||||||
|
saved_path = joinpath(job_dir, "input.json.gz")
|
||||||
|
@test isfile(saved_path)
|
||||||
|
saved_data = read(saved_path)
|
||||||
|
@test saved_data == compressed_data
|
||||||
|
|
||||||
|
response = HTTP.get("http://localhost:$port/jobs/123/view")
|
||||||
|
@test response.status == 200
|
||||||
|
@test String(response.body) == "OK"
|
||||||
|
|
||||||
|
# Clean up: remove the job directory
|
||||||
|
# rm(job_dir, recursive=true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function runtests()
|
||||||
|
@testset "UCJL Backend" begin
|
||||||
|
test_usage()
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
function format()
|
||||||
|
JuliaFormatter.format(basedir, verbose = true)
|
||||||
|
JuliaFormatter.format("$basedir/../../src", verbose = true)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
export runtests, format
|
||||||
|
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user