# Unit Commitment

_**[Power Systems Optimization](https://github.com/east-winds/power-systems-optimization)**_

_by Michael R. Davidson and Jesse D. Jenkins (last updated: September 29, 2020)_

This notebook will build on the economic dispatch (ED) model by introducing binary startup (called "commitment") decisions, constraints, and costs of thermal generators. Similar to ED, it minimizes the short-run production costs of meeting electricity demand, but these additions are necessary for operating systems with large amounts of (inflexible) thermal generation. They also entail significant computational trade-offs.

****

We build up the model in several stages. We first start with the simplest model that incorporates unit commitment decisions in order to understand the logic of three-variable commitment formulations.

Next, we introduce moderate complexity through the addition of ramp constraints—which we need to be modified from the ED to account for startups and shutdowns.

Finally, we model a more realistic unit commitment that includes reserves—an important feature of day-ahead scheduling in power systems.

## Introduction to unit commitment

Engineering considerations severely limit the possible output ranges of power plants. System operators need to be aware of these limitations when scheduling generation to meet demand. Thermal power plants, in particular, due to their complex designs, thermodynamic cycles, and material properties, can be particularly challenging. In practice, due to the times involved in bringing these power plants online, much of this scheduling is done day-ahead, which gives rise to the need for a day-ahead market.

**Unit commitment** (UC) is the problem of minimizing short-run costs of production inclusive of production and startup costs, in order to meet a given demand and considering relevant engineering constraints of the generators. It is built on the ED model in that it typically considers all of the same variables in addition to the new startup-related variables and constraints—hence, it is also sometimes called unit commitment and economic dispatch (UCED). This notebook, for simplicity, only considers a single-bus case, while typical formulations usually include a simplified network representation.

## Simple unit commitment

We start with the simplest case that incorporates unit commitment, building on the `economic_dispatch_multi` problem of the [Economic Dispatch notebook](04-Economic-Dispatch.ipynb).

$$
\begin{align}
\min \ & \sum_{g \in G, t \in T} VarCost_g \times GEN_{g,t} + \sum_{g \in G_{thermal}, t \in T} StartUpCost_g \times START_{g,t} \\
\end{align}
$$
$$
\begin{align}
\text{s.t.} & \\
 & \sum_{g} GEN_{g,t} = Demand_t & \forall \quad t \in T \\
 & GEN_{g,t} \leq Pmax_{g,t} & \forall \quad g \notin G_{thermal} , t \in T \\
 & GEN_{g,t} \geq Pmin_{g,t} & \forall \quad g \notin G_{thermal} , t \in T \\
 & GEN_{g,t} \leq Pmax_{g,t} \times COMMIT_{g,t} & \forall \quad g \in G_{thermal} , t \in T \\
 & GEN_{g,t} \geq Pmin_{g,t} \times COMMIT_{g,t} & \forall \quad g \in G_{thermal} , t \in T \\
 & COMMIT_{g,t} \geq \sum_{t'≥t-MinUp_g}^{t} START_{g,t} & \forall \quad g \in G_{thermal} , t \in T \\
 & 1-COMMIT_{g,t} \geq \sum_{t'≥t-MinDown_g}^{t} SHUT_{g,t} &\forall \quad g \in G_{thermal} , t \in T \\
  & COMMIT_{g,t+1} - COMMIT_{g,t} =&\\
 & \quad START_{g,t+1} - SHUT_{g,t+1} &\forall \quad G_{thermal} \in G , t = 1..T-1
\end{align}
$$

The **decision variables** in the above problem:

- $GEN_{g}$, generation (in MW) produced by each generator, $g$
- $START_{g,t}$, startup decision (binary) of thermal generator $g$ at time $t$
- $SHUT_{g,t}$, shutdown decision (binary) of thermal generator $g$ at time $t$
- $COMMIT_{g,t}$, commitment status (binary) of generator $g$ at time $t$

The **parameters** are:

- $Pmin_g$, the minimum operating bounds for generator $g$ (based on engineering or natural resource constraints)
- $Pmax_g$, the maximum operating bounds for generator $g$ (based on engineering or natural resource constraints)
- $Demand$, the demand (in MW)
- $VarCost_g = VarOM_g + HeatRate_g \times FuelCost_g$, the variable cost of generator $g$
- $StartUpCost_g$, the startup cost of generator $g$
- $MinUp_g$, the minimum up time of generator $g$, or the minimum time after start-up before a unit can shut down
- $MinDown_g$, the minimum down time of generator $g$, or the minimum time after shut-down before a unit can start again

In addition, we introduce a few different sets:

- $G$, the set of all generators
- $G_{thermal} \subset G$, the subset of thermal generators for which commitment is necessary
- $T$, the set of all time periods over which we are optimizing commitment and dispatch decisions

Finally, the **three-variable commitment equations** capture the basic logic of commitment:

- Units incur costs when they startup (not when they shutdown)
- Units must stay on (and off) for a minimum period of time—in lieu of explicitly enforcing a startup trajectory
- Some summations (simplified here) will need to be modified near the beginning of the time period

There are some further resources at the bottom for alternative and/or more complex formulations of the UC.

Now, let's implement UC.

### 1. Load packages

```julia
using JuMP, GLPK
using Plots; plotly();
using VegaLite  # to make some nice plots
using DataFrames, CSV, PrettyTables
ENV["COLUMNS"]=120; # Set so all columns of DataFrames and Matrices are displayed
```

    ┌ Info: For saving to png with the Plotly backend ORCA has to be installed.
    └ @ Plots /Users/jdj2/.julia/packages/Plots/M1wcx/src/backends.jl:373

### 2. Load and format data

We will use data loosely based on San Diego Gas and Electric (SDG&E, via the [PowerGenome](https://github.com/gschivley/PowerGenome) data platform) including a few neighboring generators and adjustments to make the problem easy to solve, consisting of:

- 33 generators (we added a few more to ensure we can provide enough reserves)
- estimated hourly demand for 2020 (net load at the transmission substation level after subtracting 600MW of behind-the-meter solar from original demand)
- variable generation capacity factors
- estimated natural gas fuel costs

In order to demonstrate the impacts of unit commitment, we will keep our high solar sensitivity case from the [Economic Dispatch notebook](04-Economic-Dispatch.ipynb) (with 3,500 MW of solar PV) to produce large variations in net load (demand less available renewable supply) required to meet demand (similar to [California's infamous "Duck Curve"](https://www.caiso.com/Documents/FlexibleResourcesHelpRenewables_FastFacts.pdf)).

```julia
datadir = joinpath("uc_data")
gen_info = CSV.read(joinpath(datadir,"Generators_data.csv"), DataFrame);
fuels = CSV.read(joinpath(datadir,"Fuels_data.csv"), DataFrame);
loads = CSV.read(joinpath(datadir,"Demand.csv"), DataFrame);
gen_variable = CSV.read(joinpath(datadir,"Generators_variability.csv"), DataFrame);

# Rename all columns to lowercase (by convention)
for f in [gen_info, fuels, loads, gen_variable]
    rename!(f,lowercase.(names(f)))
end
```

**Construct generator dataframe**

```julia
# Keep columns relevant to our UC model
select!(gen_info, 1:26) # columns 1:26
gen_df = outerjoin(gen_info,  fuels, on = :fuel) # load in fuel costs and add to data frame
rename!(gen_df, :cost_per_mmbtu => :fuel_cost)   # rename column for fuel cost
gen_df.fuel_cost[ismissing.(gen_df[:,:fuel_cost])] .= 0

# create "is_variable" column to indicate if this is a variable generation source (e.g. wind, solar):
gen_df.is_variable = false
gen_df[in(["onshore_wind_turbine","small_hydroelectric","solar_photovoltaic"]).(gen_df.resource),
    :is_variable] .= true;

# create full name of generator (including geographic location and cluster number)
#  for use with variable generation dataframe
gen_df.gen_full = lowercase.(gen_df.region .* "_" .* gen_df.resource .* "_" .* string.(gen_df.cluster) .* ".0");

# remove generators with no capacity (e.g. new build options that we'd use if this was capacity expansion problem)
gen_df = gen_df[gen_df.existing_cap_mw .> 0,:];
```

**Modify load and variable generation dataframes**

```julia
# 1. Convert from GMT to GMT-8
gen_variable.hour = mod.(gen_variable.hour .- 9, 8760) .+ 1
sort!(gen_variable, :hour)
loads.hour = mod.(loads.hour .- 9, 8760) .+ 1
sort!(loads, :hour);

# 2. Convert from "wide" to "long" format
gen_variable_long = stack(gen_variable,
                        Not(:hour),
                        variable_name=:gen_full,
                        value_name=:cf);
# Now we have a "long" dataframe;
```

### 3. Create solver function

As a utility function, since we do this alot, we'll create a function to convert JuMP AxisArray outputs with two indexes to DataFrames.

```julia
#=
Function to convert JuMP outputs (technically, AxisArrays) with two-indexes to a dataframe
Inputs:
    var -- JuMP AxisArray (e.g., value.(GEN))
Reference: https://jump.dev/JuMP.jl/v0.19/containers/
=#
function value_to_df_2dim(var)
    solution = DataFrame(var.data)
    ax1 = var.axes[1]
    ax2 = var.axes[2]
    cols = names(solution)
    insertcols!(solution, 1, :r_id => ax1)
    solution = stack(solution, Not(:r_id), variable_name=:hour)
    solution.hour = foldl(replace, [cols[i] => ax2[i] for i in 1:length(ax2)], init=solution.hour)
    rename!(solution, :value => :gen)
    solution.hour = convert.(Int64,solution.hour)
    return solution
end
```

    value_to_df_2dim (generic function with 1 method)

Then we'll create a function to create and solve a simple unit commitment problem with specified input data.

```julia
#=
Function to solve simple unit commitment problem (commitment equations)
Inputs:
    gen_df -- dataframe with generator info
    loads  -- load by time
    gen_variable -- capacity factors of variable generators (in "long" format)
=#
function unit_commitment_simple(gen_df, loads, gen_variable)
    UC = Model(GLPK.Optimizer)

    # We reduce the MIP gap tolerance threshold here to increase tractability
    # Here we set it to a 1% gap, meaning that we will terminate once we have
    # a feasible integer solution guaranteed to be within 1% of the objective
    # function value of the optimal solution.
    # Note that GLPK's default MIP gap is 0.0, meaning that it tries to solve
    # the integer problem to optimality, which can take a LONG time for
    # any complex problem. So it is important to set this to a realistic value.
    set_optimizer_attribute(UC, "mip_gap", 0.01)

    # Define sets based on data
    # Note the creation of several different sets of generators for use in
    # different equations.
        # Thermal resources for which unit commitment constraints apply
    G_thermal = gen_df[gen_df[!,:up_time] .> 0,:r_id]
        # Non-thermal resources for which unit commitment constraints do NOT apply
    G_nonthermal = gen_df[gen_df[!,:up_time] .== 0,:r_id]
        # Variable renewable resources
    G_var = gen_df[gen_df[!,:is_variable] .== 1,:r_id]
        # Non-variable (dispatchable) resources
    G_nonvar = gen_df[gen_df[!,:is_variable] .== 0,:r_id]
        # Non-variable and non-thermal resources
    G_nt_nonvar = intersect(G_nonvar, G_nonthermal)
        # Set of all generators (above are all subsets of this)
    G = gen_df.r_id
        # All time periods (hours) over which we are optimizing
    T = loads.hour
        # A subset of time periods that excludes the last time period
    T_red = loads.hour[1:end-1]  # reduced time periods without last one

    # Generator capacity factor time series for variable generators
    gen_var_cf = innerjoin(gen_variable,
                    gen_df[gen_df.is_variable .== 1 ,
                        [:r_id, :gen_full, :existing_cap_mw]],
                    on = :gen_full)

    # Decision variables
    @variables(UC, begin
            # Continuous decision variables
        GEN[G, T]  >= 0     # generation
            # Bin = binary variables;
            # the following are all binary decisions that
            # can ONLY take the values 0 or 1
            # The presence of these discrete decisions makes this an MILP
        COMMIT[G_thermal, T], Bin # commitment status (Bin=binary)
        START[G_thermal, T], Bin  # startup decision
        SHUT[G_thermal, T], Bin   # shutdown decision
    end)

    # Objective function
        # Sum of variable costs + start-up costs for all generators and time periods
    @objective(UC, Min,
        sum( (gen_df[gen_df.r_id .== i,:heat_rate_mmbtu_per_mwh][1] * gen_df[gen_df.r_id .== i,:fuel_cost][1] +
            gen_df[gen_df.r_id .== i,:var_om_cost_per_mwh][1]) * GEN[i,t]
                        for i in G_nonvar for t in T) +
        sum(gen_df[gen_df.r_id .== i,:var_om_cost_per_mwh][1] * GEN[i,t]
                        for i in G_var for t in T)  +
        sum(gen_df[gen_df.r_id .== i,:start_cost_per_mw][1] *
            gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
            START[i,t]
                        for i in G_thermal for t in T)
    )

    # Demand balance constraint (supply must = demand in all time periods)
    @constraint(UC, cDemand[t in T],
        sum(GEN[i,t] for i in G) == loads[loads.hour .== t,:demand][1])

    # Capacity constraints
      # 1. thermal generators requiring commitment
    @constraint(UC, Cap_thermal_min[i in G_thermal, t in T],
        GEN[i,t] >= COMMIT[i, t] * gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                        gen_df[gen_df.r_id .== i,:min_power][1])
    @constraint(UC, Cap_thermal_max[i in G_thermal, t in T],
        GEN[i,t] <= COMMIT[i, t] * gen_df[gen_df.r_id .== i,:existing_cap_mw][1])

      # 2. non-variable generation not requiring commitment
    @constraint(UC, Cap_nt_nonvar[i in G_nt_nonvar, t in T],
        GEN[i,t] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1])

      # 3. variable generation, accounting for hourly capacity factor
    @constraint(UC, Cap_var[i in 1:nrow(gen_var_cf)],
            GEN[gen_var_cf[i,:r_id], gen_var_cf[i,:hour] ] <=
                        gen_var_cf[i,:cf] *
                        gen_var_cf[i,:existing_cap_mw])

    # Unit commitment constraints
      # 1. Minimum up time
    @constraint(UC, Startup[i in G_thermal, t in T],
        COMMIT[i, t] >= sum(START[i, tt]
                        for tt in intersect(T,
                            (t-gen_df[gen_df.r_id .== i,:up_time][1]):t)))

      # 2. Minimum down time
    @constraint(UC, Shutdown[i in G_thermal, t in T],
        1-COMMIT[i, t] >= sum(SHUT[i, tt]
                        for tt in intersect(T,
                            (t-gen_df[gen_df.r_id .== i,:down_time][1]):t)))

      # 3. Commitment state
    @constraint(UC, CommitmentStatus[i in G_thermal, t in T_red],
        COMMIT[i,t+1] - COMMIT[i,t] == START[i,t+1] - SHUT[i,t+1])

    # Solve statement (! indicates runs in place)
    optimize!(UC)

    # Generation solution and convert to data frame
    # with our helper function defined above
    gen = value_to_df_2dim(value.(GEN))

    # Commitment status solution and convert to data frame
    commit = value_to_df_2dim(value.(COMMIT))

    # Calculate curtailment = available wind and/or solar output that
    # had to be wasted due to operating constraints
    curtail = innerjoin(gen_var_cf, gen, on = [:r_id, :hour])
    curtail.curt = curtail.cf .* curtail.existing_cap_mw - curtail.gen

    # Return the solution parameters and objective
    return (
        gen,
        commit,
        curtail,
        cost = objective_value(UC),
        status = termination_status(UC)
    )

end
```

    unit_commitment_simple (generic function with 1 method)

### 4. Solve a day's unit commitment

```julia
# A spring day
n=100
T_period = (n*24+1):((n+1)*24)

# High solar case: 3,500 MW
gen_df_sens = copy(gen_df)
gen_df_sens[gen_df_sens.resource .== "solar_photovoltaic",
    :existing_cap_mw] = 3500

loads_multi = loads[in.(loads.hour,Ref(T_period)),:]
gen_variable_multi = gen_variable_long[in.(gen_variable_long.hour,Ref(T_period)),:]

solution = unit_commitment_simple(gen_df_sens, loads_multi, gen_variable_multi);
```

```julia
# Add in BTM solar and curtailment and plot results

sol_gen = innerjoin(solution.gen,
                    gen_df[!, [:r_id, :resource]],
                    on = :r_id)
sol_gen = combine(groupby(sol_gen, [:resource, :hour]),
            :gen => sum)

# Rename generators (for plotting purposes)
sol_gen[sol_gen.resource .== "solar_photovoltaic", :resource] .= "_solar_photovoltaic"
sol_gen[sol_gen.resource .== "onshore_wind_turbine", :resource] .= "_onshore_wind_turbine"
sol_gen[sol_gen.resource .== "small_hydroelectric", :resource] .= "_small_hydroelectric"

# BTM solar
btm = DataFrame(resource = repeat(["_solar_photovoltaic_btm"]; outer=length(T_period)),
    hour = T_period,
    gen_sum = gen_variable_multi[gen_variable_multi.gen_full .== "wec_sdge_solar_photovoltaic_1.0",:cf] * 600)
append!(sol_gen, btm)

# Curtailment
curtail = combine(groupby(solution.curtail, [:hour]),
            :curt => sum)
curtail.resource = "_curtailment"
rename!(curtail, :curt_sum => :gen_sum)
append!(sol_gen, curtail[:,[:resource, :hour, :gen_sum]])

# Rescale hours
sol_gen.hour = sol_gen.hour .- T_period[1]

sol_gen |>
@vlplot(:area,
    x=:hour, y={:gen_sum, stack=:zero},
    color={"resource:n", scale={scheme="category10"}})
```

Notice that the combined cycle plants flatten out during the day, but don't shut down due to the need for ramping capabilities in the late afternoon/evening, when solar output falls off and evening demand remains strong. This leads to some curtailment of solar from 11:00 - 16:00, which was not present in the Economic Dispatch model. All curtailment is lumped together as the model does not accurately distinguish between whether wind, solar or hydro is curtailed, which in practice is up to the system operator and depends on a variety of factors such as location, interconnection voltage, etc.

We can examine the commitment status of various units by examining the results in `solution.commit`.

```julia
sol_commit = innerjoin(solution.commit,
                    gen_df[!, [:r_id, :resource]],
                    on = :r_id)
sol_commit = combine(groupby(sol_commit, [:resource, :hour]),
            :gen => sum)
sol_commit.hour = sol_commit.hour .- T_period[1]

sol_commit |>
@vlplot(:area,
    x=:hour, y={:gen_sum, stack=:zero},
    color={"resource:n", scale={scheme="category10"}})
```

Note that units shutdown during the solar period and startup for the evening peak. However, due to the commitment constraints, not all natural gas plants can be decommitted, resulting in the observed curtailment. A large number of combustion turbines turn on to meet the evening peak.

## Moderate complexity unit commitment

We expand the above unit commitment with ramp equations. These must be modified to prevent ramp violations during the startup process. In order to accommodate this, we introduce an **auxiliary variable** $GENAUX_{g,t}$ defined as the generation above the minimum output (if committed). $GENAUX_{g,t} = GEN_{g,t} = 0$ if the unit is not committed. Auxiliary variables are for convenience (we could always write the whole problem in terms of the original decision variables), but they help de-clutter the model and in general won't add too much computational penalty. The following constraints are added:

$$
\begin{align}
 & GENAUX_{g,t} = GEN_{g,t} - Pmin_{g,t}COMMIT_{g,t} & \forall \quad g \in G_{thermal} , t \in T \\
 & GENAUX_{g,t+1} - GENAUX_{g,t} \leq RampUp_{g} & \forall \quad g \in G_{thermal} , t = 1..T-1 \\
  & GENAUX_{g,t} - GENAUX_{g,t+1} \leq RampDn_{g} & \forall \quad g \in G_{thermal} , t = 1..T-1 \\
 & GEN_{g,t+1} - GEN_{g,t} \leq RampUp_{g} & \forall \quad g \notin G_{thermal} , t = 1..T-1 \\
  & GEN_{g,t} - GEN_{g,t+1} \leq RampDn_{g} & \forall \quad g \notin G_{thermal} , t = 1..T-1
\end{align}
$$

The creation of this auxiliary variable $GENAUX$ helps us avoid violating ramping constraints when we start up our units and their generation immediately jumps up to their minium output level or during shut-down when a unit drops from minimum output (or above) to zero.

Note you may encounter alternative formulations in the literature to address this start-up and shut-down that modify the right-hand side of the traditional ramping constraint on total generation to account for the additional step change in output during start-up or shut-down periods. The above formulation is conceptually simple and works well.

### 3. Create solver function

(We reuse steps 1 and 2 above to load packages and data.)

```julia
#=
Function to solve moderate complexity unit commitment problem (commitment and ramp equations)
Inputs:
    gen_df -- dataframe with generator info
    loads  -- load by time
    gen_variable -- capacity factors of variable generators (in "long" format)
=#
function unit_commitment_ramp(gen_df, loads, gen_variable)
    UC = Model(GLPK.Optimizer)
    set_optimizer_attribute(UC, "mip_gap", 0.01) #1% MIP gap

    # Define sets based on data
    # Note the creation of several different sets of generators for use in
    # different equations.
    G_thermal = gen_df[gen_df[!,:up_time] .> 0,:r_id]
    G_nonthermal = gen_df[gen_df[!,:up_time] .== 0,:r_id]
    G_var = gen_df[gen_df[!,:is_variable] .== 1,:r_id]
    G_nonvar = gen_df[gen_df[!,:is_variable] .== 0,:r_id]
    G_nt_nonvar = intersect(G_nonvar, G_nonthermal)
    G = gen_df.r_id
    T = loads.hour
    T_red = loads.hour[1:end-1]  # reduced time periods without last one

    # Generator capacity factor time series for variable generators
    gen_var_cf = innerjoin(gen_variable,
                    gen_df[gen_df.is_variable .== 1 ,
                        [:r_id, :gen_full, :existing_cap_mw]],
                    on = :gen_full)

    # Decision variables
    @variables(UC, begin
        GEN[G, T]  >= 0     # generation
        GENAUX[G_thermal, T] >= 0 # auxiliary generation variable
        COMMIT[G_thermal, T], Bin # commitment status (Bin=binary)
        START[G_thermal, T], Bin  # startup decision
        SHUT[G_thermal, T], Bin   # shutdown decision
    end)

    # Objective function
    @objective(UC, Min,
        sum( (gen_df[gen_df.r_id .== i,:heat_rate_mmbtu_per_mwh][1] * gen_df[gen_df.r_id .== i,:fuel_cost][1] +
            gen_df[gen_df.r_id .== i,:var_om_cost_per_mwh][1]) * GEN[i,t]
                        for i in G_nonvar for t in T) +
        sum(gen_df[gen_df.r_id .== i,:var_om_cost_per_mwh][1] * GEN[i,t]
                        for i in G_var for t in T) +
        sum(gen_df[gen_df.r_id .== i,:start_cost_per_mw][1] *
            gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
            START[i,t]
                        for i in G_thermal for t in T)
    )

    # Demand constraint
    @constraint(UC, cDemand[t in T],
        sum(GEN[i,t] for i in G) == loads[loads.hour .== t,:demand][1])

    # Capacity constraints (thermal generators requiring commitment)
    @constraint(UC, Cap_thermal_min[i in G_thermal, t in T],
        GEN[i,t] >= COMMIT[i, t] * gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                        gen_df[gen_df.r_id .== i,:min_power][1])
    @constraint(UC, Cap_thermal_max[i in G_thermal, t in T],
        GEN[i,t] <= COMMIT[i, t] * gen_df[gen_df.r_id .== i,:existing_cap_mw][1])

    # Capacity constraints (non-variable generation not requiring commitment)
    @constraint(UC, Cap_nt_nonvar[i in G_nt_nonvar, t in T],
        GEN[i,t] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1])

    # Capacity constraints (variable generation)
    @constraint(UC, Cap_var[i in 1:nrow(gen_var_cf)],
            GEN[gen_var_cf[i,:r_id], gen_var_cf[i,:hour] ] <=
                        gen_var_cf[i,:cf] *
                        gen_var_cf[i,:existing_cap_mw])

    # Unit commitment constraints
    @constraint(UC, Startup[i in G_thermal, t in T],
        COMMIT[i, t] >= sum(START[i, tt]
                        for tt in intersect(T,
                            (t-gen_df[gen_df.r_id .== i,:up_time][1]):t)))

    @constraint(UC, Shutdown[i in G_thermal, t in T],
        1-COMMIT[i, t] >= sum(SHUT[i, tt]
                        for tt in intersect(T,
                            (t-gen_df[gen_df.r_id .== i,:down_time][1]):t)))

    @constraint(UC, CommitmentStatus[i in G_thermal, t in T_red],
        COMMIT[i,t+1] - COMMIT[i,t] == START[i,t+1] - SHUT[i,t+1])

    # New auxiliary variable GENAUX for generation above the minimum output level
    # for committed thermal units (only created for thermal generators)
    @constraint(UC, AuxGen[i in G_thermal, t in T],
        GENAUX[i,t] == GEN[i,t] - COMMIT[i, t] * gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                        gen_df[gen_df.r_id .== i,:min_power][1])

    # Ramp equations for thermal generators (constraining GENAUX)
    @constraint(UC, RampUp_thermal[i in G_thermal, t in T_red],
        GENAUX[i,t+1] - GENAUX[i,t] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                                 gen_df[gen_df.r_id .== i,:ramp_up_percentage][1] )

    @constraint(UC, RampDn_thermal[i in G_thermal, t in T_red],
        GENAUX[i,t] - GENAUX[i,t+1] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                                 gen_df[gen_df.r_id .== i,:ramp_dn_percentage][1] )

    # Ramp equations for non-thermal generators (constraining total generation GEN)
    @constraint(UC, RampUp_nonthermal[i in G_nonthermal, t in T_red],
        GEN[i,t+1] - GEN[i,t] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                                 gen_df[gen_df.r_id .== i,:ramp_up_percentage][1] )

    @constraint(UC, RampDn[i in G, t in T_red],
        GEN[i,t] - GEN[i,t+1] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                                 gen_df[gen_df.r_id .== i,:ramp_dn_percentage][1] )

    # Solve statement (! indicates runs in place)
    optimize!(UC)

    # Generation solution
    gen = value_to_df_2dim(value.(GEN))

    # Commitment status solution
    commit = value_to_df_2dim(value.(COMMIT))

    # Calculate curtailment
    curtail = innerjoin(gen_var_cf, gen, on = [:r_id, :hour])
    curtail.curt = curtail.cf .* curtail.existing_cap_mw - curtail.gen

    # Return the solution and objective as named tuple
    return (
        gen,
        commit,
        curtail,
        cost = objective_value(UC),
        status = termination_status(UC)
    )

end
```

    unit_commitment_ramp (generic function with 1 method)

### 4. Solve a day's unit commitment

```julia
solution = unit_commitment_ramp(gen_df_sens, loads_multi, gen_variable_multi);

# Add in BTM solar and curtailment and plot results

sol_gen = innerjoin(solution.gen,
                    gen_df[!, [:r_id, :resource]],
                    on = :r_id)
sol_gen = combine(groupby(sol_gen, [:resource, :hour]),
            :gen => sum)

# Rename generators (for plotting purposes)
sol_gen[sol_gen.resource .== "solar_photovoltaic", :resource] .= "_solar_photovoltaic"
sol_gen[sol_gen.resource .== "onshore_wind_turbine", :resource] .= "_onshore_wind_turbine"
sol_gen[sol_gen.resource .== "small_hydroelectric", :resource] .= "_small_hydroelectric"

# BTM solar
btm = DataFrame(resource = repeat(["_solar_photovoltaic_btm"]; outer=length(T_period)),
    hour = T_period,
    gen_sum = gen_variable_multi[gen_variable_multi.gen_full .== "wec_sdge_solar_photovoltaic_1.0",:cf] * 600)
append!(sol_gen, btm)

# Curtailment
curtail = combine(groupby(solution.curtail, [:hour]),
            :curt => sum)
curtail.resource = "_curtailment"
rename!(curtail, :curt_sum => :gen_sum)
append!(sol_gen, curtail[:,[:resource, :hour, :gen_sum]])

# Rescale hours
sol_gen.hour = sol_gen.hour .- T_period[1]

sol_gen |>
@vlplot(:area,
    x=:hour, y={:gen_sum, stack=:zero},
    color={"resource:n", scale={scheme="category10"}})
```

In contrast to when we added ramp constraints to the Economic Dispatch problem, adding ramp constraints to our Unit Commitment formulation does not change the solution as much (at least in this case). Much of the inflexibility here arises from the unit commitment constraints, and once committed, there is ample ramping capability. We do however get relatively more combustion turbines running to meet the evening ramp, given their faster ramp rates.

```julia
sol_commit = innerjoin(solution.commit,
                    gen_df[!, [:r_id, :resource]],
                    on = :r_id)
sol_commit = combine(groupby(sol_commit, [:resource, :hour]),
            :gen => sum)
sol_commit.hour = sol_commit.hour .- T_period[1]

sol_commit |>
@vlplot(:area,
    x=:hour, y={:gen_sum, stack=:zero},
    color={"resource:n", scale={scheme="category10"}})
```

A case involving more large coal or nuclear units with slower ramping limits than gas turbines or combined cycle plants might find ramp constraints more limiting. As in many cases, the impact of constraints is case dependent, and models must exercise some judicious application of domain knowledge (and experimentation with alternative formulations) to decide how complex your model should be to reflect important *binding* constraints that actually shape your outcomes of interest. Where constraints are non-binding or second order, it may be more practical to omit them entirely, reducing the computational intensity (solve time) of your model...

## Unit commitment with ramping and reserves

We now add spinning reserve requirements to the model. Spinning reserves refer to generation capacity that is online and able to generate (e.g., within 10-30 minutes) if needed by the system operator. (Note: in Europe, these are referred to as tertiary reserves.) The SO will establish reserve requirements to maintain sufficient capacity to respond to demand or supply forecast errors or in case of "contingencies," such as the unplanned and sudden loss of a generating station or a transmission line. The SO then typically operates a reserve market to competitively procure this available capacity. Most SO's actually define several classes of reserve products defined by their response time and period over which they may be activated.

Here we will focus on spinning reserve requirements and establish a simple set of reserve requirements for reserves up (ability to quickly increase output) and reserves down (ability to quickly reduce output):

$$
\begin{align}
 & ResReqUp_t = 300 MW + 5\% \times Demand_t  &\forall \quad t \in T \\
 & ResReqDn_t = 5\% \times Demand_t & \forall \quad t \in T
\end{align}
$$

Here, $300 MW$ is our contigency reserves, meant to ensure we have sufficient upwards ramping capability online and available to cover the unexpected loss of 300 MW worth of generation.

The 5% of demand term in the constraints above is meant to provide sufficient reserves in either upwards or downwards direction to cover errors in the demand forecast. For simplicity, we ignore solar PV and wind production in our reserve calculation, though in practice, we would want to consider solar and wind forecast errors in calculating up reserve requirements as well.

In our simple system, only thermal generators provide reserves. (This can be easily adjusted by changing the sets and defining the potential reserve contributions for different sets of resources differently, such as storage or hydro resources). The contribution of each generators to meeting reserves and the overall reserve constraint are thus given by:

$$
\begin{align}
 & RESUP_{g,t} \leq Pmax_{g,t}COMMIT_{g,t} - GEN_{g,t} & \forall \quad g \in G_{thermal} , t \in T \\
 & RESDN_{g,t} \leq GEN_{g,t} - Pmin_{g,t}COMMIT_{g,t} & \forall \quad g \in G_{thermal} , t \in T \\
 & RESUP_{g,t} \leq RampUp_{g} & \forall \quad g \in G_{thermal}, t \in T\\
 & RESDN_{g,t} \leq RampDn_{g} & \forall \quad g \in G_{thermal}, t \in T \\
 & \sum_{g \in G_{thermal}} RESUP_{g,t} \geq ResReqUp_t & \forall \quad t \in T \\
 & \sum_{g \in G_{thermal}} RESDN_{g,t} \geq ResReqDn_t & \forall \quad t \in T
\end{align}
$$

We have added two new **decision variables**:

- $RESUP_{g,t}$, up-reserve capacity (in MW) of generator $g$ at time $t$
- $RESDN_{g,t}$, down-reserve capacity (in MW) of generator $g$ at time $t$

Note that in this case, we constrain the reserve contribution for each thermal generator to be the same as their hourly ramp rates, $RampUp_{g}, RampDown_{g}$. In practice, reserve products typically require a faster response time, on the order of 10-30 minutes for "tertiary" reserves (aka spinning reserves or contingency reserves), and 5-15 minutes for "secondary" reserves (regulation reserves). Thus, depending on the reserve requirements being modeled, one might specify a distinct maximum reserve contribution for each unit that reflects their ramping capabilities over shorter time periods.

### 3. Create solver function

(We reuse steps 1 and 2 above to load packages and data.)

```julia
#=
Function to solve full unit commitment problem (commitment, ramp, reserve equations)
Inputs:
    gen_df -- dataframe with generator info
    loads  -- load by time
    gen_variable -- capacity factors of variable generators (in "long" format)
=#
function unit_commitment_full(gen_df, loads, gen_variable)
    UC = Model(GLPK.Optimizer)
    # To keep solve times to a few minutes, we increase the MIP gap tolerance
    # in this more complex case to 2%. You can play with this setting to see
    # how the MIP gap affects solve times on your computer.
    set_optimizer_attribute(UC, "mip_gap", 0.02)

    # Define sets based on data
    # Note the creation of several different sets of generators for use in
    # different equations.
    G_thermal = gen_df[gen_df[!,:up_time] .> 0,:r_id]
    G_nonthermal = gen_df[gen_df[!,:up_time] .== 0,:r_id]
    G_var = gen_df[gen_df[!,:is_variable] .== 1,:r_id]
    G_nonvar = gen_df[gen_df[!,:is_variable] .== 0,:r_id]
    G_nt_nonvar = intersect(G_nonvar, G_nonthermal)
    G = gen_df.r_id
    T = loads.hour
    T_red = loads.hour[1:end-1]  # reduced time periods without last one

    # Generator capacity factor time series for variable generators
    gen_var_cf = innerjoin(gen_variable,
                    gen_df[gen_df.is_variable .== 1 ,
                        [:r_id, :gen_full, :existing_cap_mw, :resource]],
                    on = :gen_full)
    gen_var_cf.max_gen = gen_var_cf.cf .* gen_var_cf.existing_cap_mw

    # Compute reserve requirements
    ResReqUp = Dict(T[i] => 300
              + 0.05 * loads[loads.hour .== T[i],:demand][1]
                    for i in 1:length(T))
    ResReqDn = Dict(T[i] => 0
              + 0.05 * loads[loads.hour .== T[i],:demand][1]
                    for i in 1:length(T))

    # Decision variables
    @variables(UC, begin
        GEN[G, T]  >= 0     # generation
        GENAUX[G_thermal, T] >= 0 # auxiliary generation variable
        COMMIT[G_thermal, T], Bin # commitment status (Bin=binary)
        START[G_thermal, T], Bin  # startup decision
        SHUT[G_thermal, T], Bin   # shutdown decision
        RESUP[G_thermal, T]       # up reserve capacity
        RESDN[G_thermal, T]       # down reserve capacity
    end)

    # Objective function
    @objective(UC, Min,
        sum( (gen_df[gen_df.r_id .== i,:heat_rate_mmbtu_per_mwh][1] * gen_df[gen_df.r_id .== i,:fuel_cost][1] +
            gen_df[gen_df.r_id .== i,:var_om_cost_per_mwh][1]) * GEN[i,t]
                        for i in G_nonvar for t in T) +
        sum(gen_df[gen_df.r_id .== i,:var_om_cost_per_mwh][1] * GEN[i,t]
                        for i in G_var for t in T) +
        sum(gen_df[gen_df.r_id .== i,:start_cost_per_mw][1] *
            gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
            START[i,t]
                        for i in G_thermal for t in T)
    )

    # Demand constraint
    @constraint(UC, cDemand[t in T],
        sum(GEN[i,t] for i in G) == loads[loads.hour .== t,:demand][1])

    # Capacity constraints (thermal generators requiring commitment)
    @constraint(UC, Cap_thermal_min[i in G_thermal, t in T],
        GEN[i,t] >= COMMIT[i, t] * gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                        gen_df[gen_df.r_id .== i,:min_power][1])
    @constraint(UC, Cap_thermal_max[i in G_thermal, t in T],
        GEN[i,t] <= COMMIT[i, t] * gen_df[gen_df.r_id .== i,:existing_cap_mw][1])

    # Capacity constraints (non-variable generation not requiring commitment)
    @constraint(UC, Cap_nt_nonvar[i in G_nt_nonvar, t in T],
        GEN[i,t] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1])

    # Capacity constraints (variable generation)
    @constraint(UC, Cap_var[i in 1:nrow(gen_var_cf)],
            GEN[gen_var_cf[i,:r_id], gen_var_cf[i,:hour] ] <=
                        gen_var_cf[i,:max_gen])

    # Unit commitment constraints
    @constraint(UC, Startup[i in G_thermal, t in T],
        COMMIT[i, t] >= sum(START[i, tt]
                        for tt in intersect(T,
                            (t-gen_df[gen_df.r_id .== i,:up_time][1]):t)))

    @constraint(UC, Shutdown[i in G_thermal, t in T],
        1-COMMIT[i, t] >= sum(SHUT[i, tt]
                        for tt in intersect(T,
                            (t-gen_df[gen_df.r_id .== i,:down_time][1]):t)))

    @constraint(UC, CommitmentStatus[i in G_thermal, t in T_red],
        COMMIT[i,t+1] - COMMIT[i,t] == START[i,t+1] - SHUT[i,t+1])

    # Auxiliary variable
    @constraint(UC, AuxGen[i in G_thermal, t in T],
        GENAUX[i,t] == GEN[i,t] - COMMIT[i, t] * gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                        gen_df[gen_df.r_id .== i,:min_power][1])

    # Ramp equations (thermal generators)
    @constraint(UC, RampUp_thermal[i in G_thermal, t in T_red],
        GENAUX[i,t+1] - GENAUX[i,t] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                                 gen_df[gen_df.r_id .== i,:ramp_up_percentage][1] )

    @constraint(UC, RampDn_thermal[i in G_thermal, t in T_red],
        GENAUX[i,t] - GENAUX[i,t+1] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                                 gen_df[gen_df.r_id .== i,:ramp_dn_percentage][1] )

    # Ramp equations (non-thermal generators)
    @constraint(UC, RampUp_nonthermal[i in G_nonthermal, t in T_red],
        GEN[i,t+1] - GEN[i,t] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                                 gen_df[gen_df.r_id .== i,:ramp_up_percentage][1] )

    @constraint(UC, RampDn[i in G, t in T_red],
        GEN[i,t] - GEN[i,t+1] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                                 gen_df[gen_df.r_id .== i,:ramp_dn_percentage][1] )

    # Reserve equations
    # (1) Reserves limited by committed capacity of generator
    @constraint(UC, ResUpCap[i in G_thermal, t in T],
        RESUP[i,t] <= COMMIT[i, t] * gen_df[gen_df.r_id .== i,:existing_cap_mw][1]
                        - GEN[i,t])

    @constraint(UC, ResDnCap[i in G_thermal, t in T],
        RESDN[i,t] <= GEN[i,t] -
                        COMMIT[i, t] * gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                        gen_df[gen_df.r_id .== i,:min_power][1])

    # (2) Reserves limited by ramp rates
    @constraint(UC, ResUpRamp[i in G_thermal, t in T],
        RESUP[i,t] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                          gen_df[gen_df.r_id .== i,:ramp_up_percentage][1])

    @constraint(UC, ResDnRamp[i in G_thermal, t in T],
        RESDN[i,t] <= gen_df[gen_df.r_id .== i,:existing_cap_mw][1] *
                          gen_df[gen_df.r_id .== i,:ramp_dn_percentage][1])

    # (3) Overall reserve requirements
    @constraint(UC, ResUpRequirement[t in T],
        sum(RESUP[i,t] for i in G_thermal) >=
                ResReqUp[t])

    @constraint(UC, ResDnRequirement[t in T],
        sum(RESDN[i,t] for i in G_thermal) >=
                ResReqDn[t])

    # Solve statement (! indicates runs in place)
    optimize!(UC)

    # Generation solution
    gen = value_to_df_2dim(value.(GEN))

    # Commitment status solution
    commit = value_to_df_2dim(value.(COMMIT))

    # Calculate curtailment
    curtail = innerjoin(gen_var_cf, gen, on = [:r_id, :hour])
    curtail.curt = curtail.cf .* curtail.existing_cap_mw - curtail.gen

    # Return the solution and objective as named tuple
    return (
        gen,
        commit,
        curtail,
        cost = objective_value(UC),
        status = termination_status(UC)
    )

end
```

    unit_commitment_full (generic function with 1 method)

### 4. Solve the model and plot results

**Note: this might take a little while depending on your machine.**

```julia
solution = unit_commitment_full(gen_df_sens, loads_multi, gen_variable_multi);

# Add in BTM solar and curtailment and plot results

sol_gen = innerjoin(solution.gen,
                    gen_df[!, [:r_id, :resource]],
                    on = :r_id)
sol_gen = combine(groupby(sol_gen, [:resource, :hour]),
            :gen => sum)

# Rename generators (for plotting purposes)
sol_gen[sol_gen.resource .== "solar_photovoltaic", :resource] .= "_solar_photovoltaic"
sol_gen[sol_gen.resource .== "onshore_wind_turbine", :resource] .= "_onshore_wind_turbine"
sol_gen[sol_gen.resource .== "small_hydroelectric", :resource] .= "_small_hydroelectric"

# BTM solar
btm = DataFrame(resource = repeat(["_solar_photovoltaic_btm"]; outer=length(T_period)),
    hour = T_period,
    gen_sum = gen_variable_multi[gen_variable_multi.gen_full .== "wec_sdge_solar_photovoltaic_1.0",:cf] * 600)
append!(sol_gen, btm)

# Curtailment
curtail = combine(groupby(solution.curtail, [:hour]),
            :curt => sum)
curtail.resource = "_curtailment"
rename!(curtail, :curt_sum => :gen_sum)
append!(sol_gen, curtail[:,[:resource, :hour, :gen_sum]])

# Rescale hours
sol_gen.hour = sol_gen.hour .- T_period[1]

sol_gen |>
@vlplot(:area,
    x=:hour, y={:gen_sum, stack=:zero},
    color={"resource:n", scale={scheme="category10"}})
```

We can now see that the need to maintain reserve requirements leads to greater commitment of natural gas units throughout the day. The addition of CCGTs and GTs to meet reserve requirements leads to greater curtailment of wind and solar in the afternoon hours. We also see more CT units committed during the morning ramp as well as the afternoon hours to help provide reserves.

```julia
sol_commit = innerjoin(solution.commit,
                    gen_df[!, [:r_id, :resource]],
                    on = :r_id)
sol_commit = combine(groupby(sol_commit, [:resource, :hour]),
            :gen => sum)
sol_commit.hour = sol_commit.hour .- T_period[1]

sol_commit |>
@vlplot(:area,
    x=:hour, y={:gen_sum, stack=:zero},
    color={"resource:n", scale={scheme="category10"}})
```

### Further resources

Knueven, B., Ostrowski, J., & Watson, J.-P. (2019). On Mixed Integer Programming Formulations for the Unit Commitment Problem. Optimization Online, 91. http://www.optimization-online.org/DB_FILE/2018/11/6930.pdf

Morales-Espana, G., Latorre, J. M., & Ramos, A. (2013). Tight and Compact MILP Formulation of Start-Up and Shut-Down Ramping in Unit Commitment. IEEE Transactions on Power Systems, 28(2), 1288–1296. https://doi.org/10.1109/TPWRS.2012.2222938

Morales-España, G., Ramírez-Elizondo, L., & Hobbs, B. F. (2017). Hidden power system inflexibilities imposed by traditional unit commitment formulations. Applied Energy, 191, 223–238. https://doi.org/10.1016/j.apenergy.2017.01.089

Ostrowski, J., Anjos, M. F., & Vannelli, A. (2012). Tight Mixed Integer Linear Programming Formulations for the Unit Commitment Problem. IEEE Transactions on Power Systems, 27(1), 39–46. https://doi.org/10.1109/TPWRS.2011.2162008


---

 Boson sampling tutorial

To see how to construct and simulate a continuous-variable quantum circuit in Strawberry Fields, let’s consider the case of [boson sampling](https://en.wikipedia.org/wiki/Boson_sampling).

## Introduction to boson sampling
---

Introduced by [Aaronson and Arkhipov](https://doi.org/10.4086/toc.2013.v009a004), boson sampling presented a slight deviation from the general approach in quantum computation. Rather than presenting a theoretical model of universal quantum computation (i.e. a framework that enables quantum simulation of any arbitrary Hamiltonian), boson sampling-based devices are instead an example of an **intermediate quantum computer**, designed to experimentally implement a computation that is classically computationally hard.

Boson sampling proposes the following [quantum linear optics](https://en.wikipedia.org/wiki/Linear_optical_quantum_computing) scheme. An array of single photon sources are set up, designed to simultaneously emit single photon states into a multimode linear [interferometer](https://en.wikipedia.org/wiki/Interferometry); the results are then generated by sampling from the probability of single photon measurements from the output of the linear interferometer.

Steve Kolthammer, Justin Spring, Ian Walmsley, Department of Physics, University of Oxford

For example, consider $N$ single photon Fock states, $$|\psi\rangle=|m_1,m_2,\dots,m_N\rangle,$$ composed of $b=\sum_i m_i$ photons, incident on an $N$-mode linear interferometer, which performs the following linear transformation of the input mode creation and annihilations operators:

$$\begin{align}
    &\hat{a}^\dagger_{out_k} = \sum_{j=0}^N U_{kj}\hat{a}^\dagger_{in_j},~~~~\hat{a}_{out_k} = \sum_{j=0}^N U_{jk}^\dagger\hat{a}_{in_j}
\end{align}$$

Here, the unitary $U$ completely describes the interferometer. We will denote the output state of the linear interferometer as $|\psi'\rangle$ - thus, the probability of detecting $n_j$ photons at the $j$th output mode is given by
$$\left|\left\langle n_1,n_2,\dots,n_N\middle|{\psi'}\right\rangle\right|^2$$

The remarkable nature of the boson sampling problem becomes clear when you consider that

$$\left|\left\langle{n_1,n_2,\dots,n_N}\middle|{\psi'}\right\rangle\right|^2 = \frac{\left|\text{Per}(U_{st})\right|^2}{m_1!m_2!\cdots m_N!n_1!n_2!\cdots n_N!}$$

i.e. the sampled single photon probability distribution is proportional to the **permanent** of $U_{ST}$, a submatrix of the interferometer unitary, dependent upon the input and output Fock states.

**The Permanent**

The [permanent of a matrix](https://en.wikipedia.org/wiki/Permanent), defined by

$$\text{Per}(A) = \sum_{\sigma=S_N}\prod_{i=1}^N A_{i\sigma(i)},$$

where $S_N$ is the set of all permutations of $N$ elements, is calculated in a similar fashion to the determinant, but unlike the determinant, the signatures of the permutations are not taken into account - every permutation is taken as a positive quantity.

In graph theory, the permanent calculates the number of perfect matchings in a [bipartite graph](https://en.wikipedia.org/wiki/Bipartite_graph>) with adjacency matrix $A$.

Whilst the determinant can be calculated efficiently on classical computers, [calculation of the permanent](https://en.wikipedia.org/wiki/Computing_the_permanent) belongs to the computational complexity class #P (Valiant, 1979), which is strongly believed to be classically hard to calculate. (Surprisingly, even calculating the permanent in an *approximate* manner is a member of #P and classically hard).

Yet, the permanent of (some) matrix can be computed efficiently in the boson sampling scheme, providing a potential challenge to classical computation, and demonstrating the power of quantum computation.

## Continuous-variable implementation
---

In quantum linear optics, the multimode linear interferometer is commonly decomposed into two-mode beamsplitters (`BSgate`) and single-mode phase shifters (`Rgate`) (Reck, 1994), allowing for an almost trivial translation into a continuous-variable quantum circuit.

For example, in the case of a 4 mode interferometer, with arbitrary $4\times 4$ unitary $U$, the continuous-variable quantum circuit is given by

In the above,
* the detectors perform Fock state measurements,
* the parameters of the beamsplitters and the rotation gates determines the unitary $U$.

Note that, in order to allow for arbitrary linear unitaries for $m$ imput modes, we must have a minimum of $m+1$ columns in the beamsplitter array ([Clements, 2016](https://arxiv.org/abs/1603.08788)).

## Simulating boson sampling in Strawberry Fields
---

To see how to simulate a boson sampling circuit using the above circuit diagram, let's walk through the construction of a Strawberry Fields program.

### Importing Strawberry Fields

The first thing we need to do is import Strawberry Fields; we do this with the following import statements:

```python
import strawberryfields as sf
from strawberryfields.ops import *
```

The first import statement imports Strawberry Fields as `sf`, allowing us to access the engine and backends. The second import statement imports all available continuous-variable gates into the global namespace.

We'll also need to import $\pi$, either from the `math` module or `numpy`:

```python
from numpy import pi
```

### Engine initialisation

We can now initialise our chosen backend, engine, and quantum register, using the
`strawberryfields.new_engine()` function, which has the following syntax:

    sf.Engine(num_subsystems, reset=True, *args, **kwargs)

where

* `num_subsystems` (*int*) is the number of modes we want to initialise in our quantum register

* ``reset`` is a boolean that instructs the engine to begin the simulation with all modes reset to the vacuum state. If ``reset=False``, then this is skipped, and the engine will be run using the output simulation state from any previous engine runs.

Now that that's all sorted, time to initialise our engine:

```python
eng, q = sf.Engine(4)
```

The `new_engine` function returns two objects:
* `eng`, the quantum engine itself, and
* `q`, our 4 mode quantum register

The register is returned as a *tuple*, with each individual mode accessed via indexing. For example, `q[0]` represents the first mode.

Let's get to building our boson sampling circuit.

### Circuit construction
To prepare states and apply gates, we must be inside the context of the engine we initialised, using the `with` statement, and using the notation

    Operator(args) | mode(s)

with each successive operator applied in the correct, temporal order.

For example, to construct the boson sampling circuit

we would run:

```python
with eng:
    # prepare the input fock states
    Fock(1) | q[0]
    Fock(1) | q[1]
    Vac | q[2]
    Fock(1) | q[3]

    # rotation gates
    Rgate(0.5719) | q[0]
    Rgate(-1.9782) | q[1]
    Rgate(2.0603) | q[2]
    Rgate(0.0644) | q[3]

    # beamsplitter array
    BSgate(0.7804, 0.8578) | (q[0], q[1])
    BSgate(0.06406, 0.5165) | (q[2], q[3])
    BSgate(0.473, 0.1176) | (q[1], q[2])
    BSgate(0.563, 0.1517) | (q[0], q[1])
    BSgate(0.1323, 0.9946) | (q[2], q[3])
    BSgate(0.311, 0.3231) | (q[1], q[2])
    BSgate(0.4348, 0.0798) | (q[0], q[1])
    BSgate(0.4368, 0.6157) | (q[2], q[3])
```

This is a fair bit to process, so let's go through what's actually happening.

1. To begin with, we initialise the input state $|1,1,0,1\rangle$, by creating a single photon Fock state in modes 0, 1 and 3.
    * This is done via the `Fock()` operator.

2. Mode 2 is initialised as a vacuum state using the `Vac` operator.
    * This is *optional* - modes are automatically created in the vacuum state upon engine initialisation.

3. Next we apply the rotation gates, `Rgate(p)`, to each mode.
    * The resulting rotation in the phase space occurs in the anticlockwise direction, with angle $\phi=2\pi p$.

4. Finally, we apply the array of beamsplitters, using the `BSgate(q,p)` operator.
    * The arguments are related to the beamsplitter angles by the relationships $\theta = 2\pi q$ and $\phi=2\pi p$.
    * The transmission amplitude is then given by $t=\cos\theta$
    * The reflection amplitude is given by $r=e^{i\phi}\sin{\theta}$

Note that, in the above Strawberry Fields circuit, we have chosen **random** values for the rotation gates and the beamsplitter array.

**Aside**

Strawberry Fields gates are standard Python objects, and can be treated as such. For example, if we had needed to reuse a gate with the same parameters multiple times, we could write the following:

with eng:
    BS = BSgate(0.12,0.543)
    BS | (q[0], q[1])
    BS | (q[1], q[2])

### Running the engine
Once the circuit is constructed, you can run the engine via the `eng.run()` method:

```python
state =eng.run('fock', cutoff_dim=5)
```

where

* `backend` (*str*) represents the Strawberry Fields backend we wish to use; we have the choice of two Fock backends, Fock (`'fock'`) and Tensorflow (`'tf'`), and one gaussian backend (`'gaussian'`).

**The Strawberry Fields backends**

**Fock backends** are backends which represent the quantum state and operations via the Fock basis.
These can represent all possible continuous-variable states and operations, but also introduce numerical error due to truncation of the Fock space, and can consume more memory.

The **gaussian backend**, due to its ability to represent states and operations as gaussian objects/transforms in the phase space, can be less computationally intensive then the Fock backends.
However, it cannot represent non-gaussian operations and states (such as the cubic phase gate, fock measurements, and fock states, amongst others).

Since boson sampling requires single photon Fock states as input, we need to use a Fock backend; let's choose the standard Fock backend for this particular example.

Since we are using a Fock backend, we must also specify the Fock basis *cutoff dimension*; let's choose `cutoff_dim=5`, such that a state $|\psi\rangle$ has approximation

$$|{\psi}\rangle = \sum_{n=0}^\infty c_n|{n}\rangle \approx\sum_{n=0}^{5-1} c_n|{n}\rangle$$

in our truncated Fock basis - i.e. all information of quantum amplitudes of Fock states $|n\rangle$, with $n\geq 5$, is discarded.

**Warning**

To avoid significant numerical error when working with the Fock backend, we generally need to ensure that all initial states and gates we apply result in negligible amplitude in the Fock basis for Fock states $|{n}\rangle, ~~n\geq \texttt{cutoff_dim}$.

For example, to prepare a squeezed vacuum state in the $x$ quadrature with `cutoff_dim=10`, a squeezing factor of $r=1$ provides an acceptable approximation, since $\left|\left\langle{n}\middle|{z}\right\rangle\right|^2<0.02$ for $n\geq 10$.

**Scaling**

As we are performing a classical simualation of a quantum system, we still need to consider how the simulation scales. Using the Fock backends, memory usage and computational time will scale like $\sim D^N$, where $N$ is the number of modes and $D$ the cutoff dimension.

Ideally, we would like to choose a cutoff large enough that minimises truncation error, but small enough to reduce classical computational resources!

Note that `eng.run` can either go inside or outside the engine context used earlier, and will execute all operations within the command queue.

You can even continue to queue operations after running the engine, allowing you to break your circuit execution into multiple command queues and engine runs.

Other useful methods that can be called at any time include:

* `eng.print_queue()`: print the command queue (the operations to be applied on the next call to `eng.run`)
* `eng.print_applied()`: prints all commands applied on all prior calls to `eng.run` since the backend was last reset.
* `eng.reset_queue()`: clear all operations from the command queue.
* `eng.reset_backend()`: retains the command queue, but resets all modes to the vacuum state.
* `eng.reset()`: clear all operations from the command queue, *and* resets all modes to the vacuum state.

For example, try running the following:

```python
eng.print_applied()
```

    Fock(1), 	(reg[0])
    Fock(1), 	(reg[1])
    Vac, 	(reg[2])
    Fock(1), 	(reg[3])
    Rgate(0.5719), 	(reg[0])
    Rgate(-1.978), 	(reg[1])
    Rgate(2.06), 	(reg[2])
    Rgate(0.0644), 	(reg[3])
    BSgate(0.7804, 0.8578), 	(reg[0], reg[1])
    BSgate(0.06406, 0.5165), 	(reg[2], reg[3])
    BSgate(0.473, 0.1176), 	(reg[1], reg[2])
    BSgate(0.563, 0.1517), 	(reg[0], reg[1])
    BSgate(0.1323, 0.9946), 	(reg[2], reg[3])
    BSgate(0.311, 0.3231), 	(reg[1], reg[2])
    BSgate(0.4348, 0.0798), 	(reg[0], reg[1])
    BSgate(0.4368, 0.6157), 	(reg[2], reg[3])

## Calculating the interferometer unitary
---

Before we get into analysing the results of the Strawberry Fields simulation, let's calculate the unitary operator described by the rotation gates and beamsplitters - this gives us something to compare it to later.

For this part, we'll need the NumPy and SciPy libraries:

```python
import numpy as np
from numpy.linalg import multi_dot
from scipy.linalg import block_diag
```

### Rotation gate unitary

To start with, let's generate the matrix representing the unitary transformation of the input mode annihilation and creation operators. The rotation gates simply act as follows,

$$R(\phi)\hat{a} = \hat{a}e^{i\phi},$$

and thus the column of rotation gates has the following block diagonal form:

$$\begin{align}
    U_\phi = \left[\begin{matrix}
            e^{i\phi_1} & 0 & \cdots\\
            0 & e^{i\phi_2}  & \cdots \\
            \vdots & \vdots & \ddots \\
        \end{matrix}\right]
\end{align}$$

```python
Uphase = np.diag([np.exp(0.5719*1j),np.exp(-1.9782*1j),np.exp(2.0603*1j),np.exp(0.0644*1j)])
```

```python
np.round(Uphase,4)
```

    array([[ 0.8409+0.5412j,  0.0000+0.j    ,  0.0000+0.j    ,  0.0000+0.j    ],
           [ 0.0000+0.j    , -0.3962-0.9182j,  0.0000+0.j    ,  0.0000+0.j    ],
           [ 0.0000+0.j    ,  0.0000+0.j    , -0.4702+0.8826j,  0.0000+0.j    ],
           [ 0.0000+0.j    ,  0.0000+0.j    ,  0.0000+0.j    ,  0.9979+0.0644j]])

### Beamsplitter unitary

A single beamsplitter, acting on two input modes $(\hat{a}_1,\hat{a}_2)$, instead acts as follows:

$$\begin{align}
BS(\theta, \phi) (\hat{a}_1,\hat{a}_2) = \left[\begin{matrix}
            t & -r^*\\
            r & t\\
        \end{matrix}\right] \left[\begin{matrix}
            \hat{a}_1\\
            \hat{a}_2
        \end{matrix}\right]
\end{align}$$

where $t=\cos(\theta)$ and $r=e^{i\phi}\sin(\theta)$. Again, like, the rotation gate, beamsplitters in the same 'column' (i.e. acting on independent modes) combine as block diagonal matrices.

First of all, we need to convert the `BSgate` arguments, `q` and `p` (reproduced below for convenience),

```python
BSargs = [
    (0.7804, 0.8578),
    (0.06406, 0.5165),
    (0.473, 0.1176),
    (0.563, 0.1517),
    (0.1323, 0.9946),
    (0.311, 0.3231),
    (0.4348, 0.0798),
    (0.4368, 0.6157)
]
```

into transmission and reflection amplitudes $t$ and $r$:

```python
t_r_amplitudes = []

for q,p in BSargs:
    t = np.cos(q)
    r = np.exp(1j*p)*np.sin(q)
    t_r_amplitudes.append((t,r))
```

```python
t_r_amplitudes
```

    [(0.71063216937901652, (0.4602032449982007+0.53217938060093872j)),
     (0.99794885977982051, (0.05566547093254029+0.031613740841832737j)),
     (0.89020561932289122, (0.45241244157971522+0.053450332364058703j)),
     (0.84565774216485023, (0.52759605355941086+0.080655981707390259j)),
     (0.99126111276958639, (0.071872018569631851+0.11061563749094347j)),
     (0.95202803513336776, (0.29017650247396554+0.097160782891430389j)),
     (0.9069543062416473, (0.41988852889473471+0.033578411105169972j)),
     (0.90611003485741559, (0.34535860197634749+0.24431954643792997j))]

Now, we can generate the unitary matrix representation for each individual beamsplitter:

```python
BSunitaries = [np.array([[t, -np.conj(r)], [r, t]]) for t,r in t_r_amplitudes]
```

before using the SciPy function `scipy.linalg.block_diag` to calculate the overall beamsplitter unitary, $U_{BS_i}$, for *each 'column'* of independent beamsplitters:

```python
# first column (2 beamsplitters)
UBS1 = block_diag(*BSunitaries[0:2])

# second column (1 beamsplitters)
UBS2 = block_diag([[1]], BSunitaries[2], [[1]])

# third column (2 beamsplitters)
UBS3 = block_diag(*BSunitaries[3:5])

# fourth column (1 beamsplitters)
UBS4 = block_diag([[1]], BSunitaries[5], [[1]])

# fifth column (2 beamsplitters)
UBS5 = block_diag(*BSunitaries[6:8])
```

Finally, we can combine the unitaries to form a single $4\times 4$ unitary via matrix multiplication;

$$U = U_{BS_5}U_{BS_4}U_{BS_3}U_{BS_2}U_{BS_1}U_{\phi}.$$

Since `numpy.dot` only supports matrix multiplication of two arrays, we instead use `numpy.linalg.multi_dot`:

```python
U = multi_dot([UBS5, UBS4, UBS3, UBS2, UBS1, Uphase])
```

```python
print(U)
```

    [[ 0.21954694-0.25653455j  0.61107685+0.52417894j -0.10270019+0.47447883j
      -0.02725023+0.03729095j]
     [ 0.45128186+0.60258291j  0.45695259+0.01230749j  0.13162587-0.45041774j
       0.03528319-0.05324427j]
     [ 0.03871009+0.49271556j -0.01921274-0.32184285j -0.24077647+0.52443283j
      -0.45838814+0.32963337j]
     [-0.15661908+0.22456857j  0.10999222-0.16375022j -0.42117984+0.18364484j
       0.81876918+0.06801566j]]

```python
print(np.round(U,4))
```

    [[ 0.2195-0.2565j  0.6111+0.5242j -0.1027+0.4745j -0.0273+0.0373j]
     [ 0.4513+0.6026j  0.4570+0.0123j  0.1316-0.4504j  0.0353-0.0532j]
     [ 0.0387+0.4927j -0.0192-0.3218j -0.2408+0.5244j -0.4584+0.3296j]
     [-0.1566+0.2246j  0.1100-0.1638j -0.4212+0.1836j  0.8188+0.068j ]]

Examining the output of the above cell, you should have the result

$$\begin{equation}
    U = \left[\begin{matrix}
        0.2195-0.2565i & 0.6111+0.5242i & -0.1027+0.4745i & -0.0273+0.0373i\\
        0.4513+0.6026i & 0.4570+0.0123i & 0.1316-0.4504i & 0.0353-0.0532i\\
        0.0387+0.4927i & -0.0192-0.3218i & -0.2408+0.5244i & -0.4584+0.3296i\\
        -0.1566+0.2246i & 0.1100-0.1638i & -0.4212+0.1836i & 0.8188+0.068i
    \end{matrix}\right]
\end{equation}$$

This represents the unitary transformation of the input creation and annihilation operators of the linear interferometer.

## Analysis
---

Now that we have the interferometer unitary transformation $U$, as well as the simulation results, let's compare the two, and see if the boson sampling result,

$$\left|\left\langle{n_1,n_2,\dots,n_N}\middle|{\psi'}\right\rangle\right|^2 = \frac{\left|\text{Perm}(U_{st})\right|^2}{m_1!m_2!\cdots m_N!n_1!n_2!\cdots n_N!},$$

holds.

### Extracting results from Strawberry Fields

First, let's extract the joint Fock state probabilities from our Strawberry fields simulation:

```python
probs = state.all_fock_probs()
```

Here, the function `all_fock_state_probs()` returns a size $5\times 5\times 5\times 5$ array (due to having 4 particles with cutoff dimension 5) containing the Fock state probabilities of the output state,

$$\left\langle i_1,i_2,\dots,i_N \middle| {\psi'} \middle| j_1,j_2,\dots,j_N\right\rangle$$

For example, the probability $\left|\left\langle{2,0,0,1}\middle |{\psi'}\right\rangle\right|^2$ can be extracted via

```python
probs[2,0,0,1]
```

    0.1064419272464233

### Comparing SF result to the Permanent

Before we can calculate the right hand side of the boson sampling equation, we need a method of calculating the permanent. Since the permanent is classically hard to compute, it is not provided in either NumPy *or* SciPy, so we will use the definition provided in [SciPy Issue #7151](https://github.com/scipy/scipy/issues/7151) on GitHub:

```python
def perm(M):
    n = M.shape[0]
    d = np.ones(n)
    j =  0
    s = 1
    f = np.arange(n)
    v = M.sum(axis=0)
    p = np.prod(v)
    while (j < n-1):
        v -= 2*d[j]*M[j]
        d[j] = -d[j]
        s = -s
        prod = np.prod(v)
        p += s*prod
        f[0] = 0
        f[j] = f[j+1]
        f[j+1] = j+1
        j = f[0]
    return p/2**(n-1)
```

**Note**

This function makes use of the [Balasubramanian-Bax-Franklin-Glynn formula](https://en.wikipedia.org/wiki/Computing_the_permanent#Balasubramanian-Bax-Franklin-Glynn_formula), which scales as $\mathcal{O}(2^{n-1}n^2)$ for an $n\times n$.

Finally, how do we determine the submatrix $`U_{st}$? This isn't too hard ([Tillmann, 2013](https://doi.org/10.1038/nphoton.2013.102)); first, we calculate the submatrix $U_s$ by taking $m_k$ copies of the $k$th **columns** of $U$, where $m_k$ is the photon number of the $k$th input state.

Since our input state is $\left|{\psi}\right\rangle=\left|{1,1,0,1}\right\rangle$, we simply take single copies of the first, second, and fourth columns:

```python
U[:,[0,1,3]]
```

    array([[ 0.21954694-0.25653455j,  0.61107685+0.52417894j,
            -0.02725023+0.03729095j],
           [ 0.45128186+0.60258291j,  0.45695259+0.01230749j,
             0.03528319-0.05324427j],
           [ 0.03871009+0.49271556j, -0.01921274-0.32184285j,
            -0.45838814+0.32963337j],
           [-0.15661908+0.22456857j,  0.10999222-0.16375022j,
             0.81876918+0.06801566j]])

Next, we take $n_k$ copies of the $k$\text{th} **row** of $U_s$, where $n_k$ is the photon number of the $k$th **output** state that is measured.

Here, we our measuring probability of of detecting the output state $\left|{2,0,0,1}\right\rangle$ so we take two copies of the first row, and one copy of the last row:

```python
U[:,[0,1,3]][[0,0,3]]
```

    array([[ 0.21954694-0.25653455j,  0.61107685+0.52417894j,
            -0.02725023+0.03729095j],
           [ 0.21954694-0.25653455j,  0.61107685+0.52417894j,
            -0.02725023+0.03729095j],
           [-0.15661908+0.22456857j,  0.10999222-0.16375022j,
             0.81876918+0.06801566j]])

Now, using the permanent function we defined above, we can take the absolute value squared of the permanent, and divide by the product of the input and output state number factorials, as per the boson sampling equation.

Since $0!=1!=1$, we only need to take into account the case $2!=2$:

```python
np.abs(perm(U[:,[0,1,3]][[0,0,3]]))**2 / 2
```

    0.10644192724642332

Comparing this to the result from Strawberry Fields,

```python
BS = np.abs(perm(U[:,[0,1,3]][[0,0,3]]))**2 / 2
SF = probs[2,0,0,1]

100*np.abs(BS-SF)/BS
```

    1.3037896031031119e-14

**They agree with almost negligable error!** This is due to the high accuracy of the Fock backend, despite the Fock state truncation/cutoff.

**Note**

Due to the Fock basis truncation/cutoff dimension, the Fock backends only return an **approximate** of the Fock state joint probability, and thus only approximate the various submatrix permanents.

However, the Fock backends are still computing a classically intractable problem, as even approximating the matrix permanent remains a hard classical problem.

```python
np.abs(perm(U[:,[0,1,3]][[0,0,0]]))**2 / 6
```

    0.00094584833471324887

```python
>>> probs[3,0,0,0]
```

    0.0009458483347132479

```python
>>> np.abs(perm(U[:,[0,1,3]][[0,1,3]]))**2 / 1
```

    0.17468916048563934

```python
>>> probs[1,1,0,1]
```

    0.17468916048563926

```python

```


```python
!pip install qiskit
!pip install pytket-qiskit
!pip install qiskit[visualization]
!pip install pytket.backends.ibm

from IPython.display import clear_output
clear_output(wait=False)
```

```python
import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
sns.set_theme()

#Operator Imports

from qiskit.aqua.operators import Z, X, I, StateFn, CircuitStateFn, SummedOp
from qiskit.aqua.operators.gradients import Gradient, NaturalGradient, QFI, Hessian
from qiskit.aqua.operators.operator_base import OperatorBase
#Circuit imports
from qiskit.circuit import QuantumCircuit, QuantumRegister, Parameter, ParameterVector, ParameterExpression
from qiskit.circuit.library import EfficientSU2
# import pennylane as qml
# from pennylane import numpy as np
import matplotlib.pyplot as plt
import math
# from pennylane.templates.state_preparations import MottonenStatePreparation as msp
# from pennylane.templates.state_preparations import ArbitraryStatePreparation as asp
# from pennylane.templates import ParticleConservingU2
from functools import partial
import random
import numpy as np
from scipy.optimize import minimize
from sympy import *
init_printing(use_unicode=True)
import time as time
from qiskit import *

## pytket imports
from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit
from pytket.passes import FullPeepholeOptimise, DefaultMappingPass, SynthesiseIBM, RebaseIBM, DecomposeMultiQubitsIBM, RepeatPass, SequencePass, O2Pass, RemoveRedundancies, USquashIBM
from pytket.predicates import CompilationUnit
from pytket.extensions.qiskit import AerBackend as tket_AerBackend
from pytket.utils import Graph

```

```python
def convert_bin(num , ancilla_size = 0):
    n = ancilla_size                       # for 6 digits
    b = bin(num)[2:]
    l = len(b)
    b = str(0) * (n - l) + b
    b = b[::-1]
    return(b)

```

```python
def decompose(n,A):
  I = Matrix([[1,0],[0,1]])
  X = Matrix([[0,1],[1,0]])
  Z = Matrix([[1,0],[0,-1]])
  Y = Matrix([[0,-1],[1,0]])
  l = [I, X, Z,Y]
  ls = ['I','X','Z','Y']
  combinations = []
  comb = []
  for i in range(4):
    if n == 1 :
      combinations.append(l[i])
      comb.append(ls[i])
    elif n == 2  :
      for j in range(4):
        combinations.append(Matrix(np.kron(l[i],l[j])))
        comb.append(ls[i] + ls[j])
    elif n==3 :
      for j in range(4):
        for k in range(4):
          combinations.append(Matrix(np.kron(np.kron(l[i],l[j]),l[k])))
          comb.append(ls[i]+ls[j]+ls[k])
    elif n == 4 :
      for j in range(4):
        for k in range(4):
          for m in range(4):
            combinations.append(np.kron(np.kron(np.kron(l[i],l[j]),l[k]),l[m]))
            comb.append(ls[i]+ls[j]+ls[k]+ls[m])
    elif n == 5 :
      for j in range(4):
        for k in range(4):
          for m in range(4):
            for N in range(4):
              combinations.append(np.kron(np.kron(np.kron(np.kron(l[i],l[j]),l[k]),l[m]),l[N]))
              comb.append(ls[i]+ls[j]+ls[k]+ls[m]+ls[N])

    elif n == 6 :
      for j in range(4):
        for k in range(4):
          for m in range(4):
            for N in range(4):
              for o in range(4):
                combinations.append(np.kron(np.kron(np.kron(np.kron(np.kron(l[i],l[j]),l[k]),l[m]),l[N]),l[o]))
                comb.append(ls[i]+ls[j]+ls[k]+ls[m]+ls[N]+ls[o])
  r = len(combinations)
  d = []
  for x in range(0, r ):
      strng = 'a'+str(x+1)
      d.append(symbols(strng))
  M = zeros(pow(2,n) , pow(2,n))
  for i in range(len(combinations)):
    M = M + d[i]*combinations[i]
  M = M - A
  eq = [M[i] for i in range(r)]
  sol = list(linsolve(eq,d))
  SOL = {}
  Comb = []
  for i in range(len(sol[0])) :
    if sol[0][i] != 0 :
        SOL[comb[i]] = sol[0][i]
        Comb.append(combinations[i])
  return(Comb , list(SOL.values()) , list(SOL.keys()))
```

```python
def get_hyperparmeters(A_size,num_unitaries):
    global n_qubits , ancilla_size , n_shots , tot_qubits , ancilla_idx , steps , eta , q_delta , rng_seed
    n_qubits = int(A_size)                        # Number of system qubits
    ancilla_size = int(np.ceil(np.log2(num_unitaries)))                # Number of ancillary qubit
    n_shots = 10 ** 6                                  # Number of quantum measurements
    tot_qubits = int(n_qubits + ancilla_size)                          # System + ancillary qubits
    ancilla_idx = 0                                    # Index of the first ancillary qubit
    steps = 1000                                       # Number of optimization steps
    eta = 0.1                                         # Learning rate
    q_delta = 0.001                                    # Initial spread of random quantum weights
    rng_seed = 0                                       #  Seed for random number generator
```

```python
I = [[1,0],[0,1]]
X = [[0,1],[1,0]]
Z = [[1,0],[0,-1]]

A = np.kron(np.kron(I,I),I) + 0.2*np.kron((np.kron(I,Z)),X) + 0.2*np.kron(np.kron(I,I),X)
Unitaries , coefficients , keys =  decompose(int(np.log2(np.sqrt(A.size))),A)
get_hyperparmeters(np.ceil(np.log2(np.sqrt(A.size))) , len(coefficients))
```

```python
def UC():
    global c
    c = [0] + coefficients
    for i in range(len(c) , 2**ancilla_size):
        c.append(0)
    s_c = sum(c)
    c = [ np.sqrt(float(_ /s_c)) for _ in c]
    uc = QuantumCircuit(ancilla_size ,name = 'UC')
    uc.initialize(c , [_ for _ in range(ancilla_size)])
    uc = compiler.transpile(uc , basis_gates = ['u3','cx'],optimization_level = 3)
    return(uc)

def UCD():
    ucd = UC()
    ucd = ucd.inverse()
    return(ucd)

def UB():
    ub = QuantumCircuit(n_qubits ,name= 'B_dg')
    ub.h([_ for _ in range(n_qubits)])
    return(ub)
```

```python
def make_Ai(key,control_list):
    Ai = QuantumCircuit(tot_qubits)
    key = key[::-1]
    for _ in range(len(key)):
        if key[_] == 'I':
            q = QuantumCircuit(1, name = 'I')
            MCX = qiskit.circuit.library.MCMT(q.decompose().to_gate(),len(control_list),1)
            apply_bits = control_list + [ancilla_size + _]
            Ai.append(MCX,apply_bits)
        if key[_] == 'X':
            MCX = qiskit.circuit.library.MCMT(Ai.x,len(control_list),1)
            apply_bits = control_list + [ancilla_size + _]
            Ai.append(MCX,apply_bits)
        if key[_] == 'Z':
            MCX = qiskit.circuit.library.MCMT(Ai.z,len(control_list),1)
            apply_bits = control_list + [ancilla_size + _]
            Ai.append(MCX,apply_bits)
        if key[_] == 'Y':
            MCX = qiskit.circuit.library.MCMT(Ai.y,len(control_list),1)
            apply_bits = control_list + [ancilla_size + _]
            Ai.append(MCX,apply_bits)
    return(Ai.decompose())
```

```python
def CA():
    qc = QuantumCircuit(tot_qubits,name ='CA')
    for idx in range(1,len(keys)+1):
        control_bin = convert_bin(idx,ancilla_size)
        control_size = len([ _ for _ in control_bin if _ == '1' ])
        control_bits = []
        for j in range(len(control_bin)):
            if control_bin[j] == '1':
                control_bits.append(j)
        working_bits = [_  for _ in range(tot_qubits) ]
        qc.append(make_Ai(keys[idx-1],control_bits),working_bits)
        qc.barrier()
    return(qc.decompose())
```

```python
def var_circuit(parameters):
    vc = QuantumCircuit(tot_qubits,name = 'var_circuit')
    for idx in range(1,len(keys)+1):
        control_bin = convert_bin(idx,ancilla_size)
        control_size = len([ _ for _ in control_bin if _ == '1' ])
        control_bits = []
        for j in range(len(control_bin)):
            if control_bin[j] == '1':
                control_bits.append(j)
        for n_idx in range(n_qubits):
            q = QuantumCircuit(1,name = 'cry')
            q.ry(parameters[idx-1][n_idx],0)
            working_bits = control_bits + [n_idx + ancilla_size]
            mcry = qiskit.circuit.library.MCMT(q.to_gate(),len(control_bits),1)
            vc.append(mcry,working_bits)
        vc.barrier()
    return(vc.decompose())
```

```python
def full_circuit(parameters):
    FC =  QuantumCircuit(tot_qubits)
    tot_qubits_list = [_ for _ in range(tot_qubits)]
    ancilla_list = [_ for _ in range(ancilla_size)]
    system_list =  [_ + ancilla_size for _ in range(n_qubits)]

    FC.append(UC(), ancilla_list)
    FC.barrier()

    FC.append(var_circuit(parameters) , tot_qubits_list)
    FC.barrier()

    FC.append(CA(),tot_qubits_list)
    FC.barrier()

    #FC.append(UB(),system_list)
    #FC.barrier()

    FC.append(UCD(),ancilla_list)

    ## UPDATE - tranpiled in u3 and cx gates only
    FC = compiler.transpile(FC , basis_gates = ['u3','cx'],optimization_level = 3)

    return(FC)
```

```python
## UPDATE - output from tket compiled circuit is passed through
##          this function to match the dtype same as qiskit o/p

def convert_format_to_qiskit(job):
    keys = list(job.keys())

    key_list = []
    value_list = []

    for key in keys:
        value_list.append(job[key])
        res = ''
        for i in key:
            res += str(i)
        key_list.append(res)

    dic = dict(zip(key_list, value_list))
    return dic
```

```python
def global_ground_qiskit(weights):

    num_parameters = len(keys) * n_qubits
    parameters = []
    for i in range(num_parameters):
        parameters.append(Parameter('w_' + str(i)))

    weights = weights
    value_dict = {}
    for i in range(len(parameters)) :
        value_dict[parameters[i]] = float(weights[i])

    parameters = np.array(parameters).reshape(len(keys),n_qubits)

    circuit = QuantumCircuit(tot_qubits,n_qubits)
    circuit.append(full_circuit(parameters),[_ for _ in range(tot_qubits)])
    circuit = circuit.bind_parameters(value_dict)
    circuit.measure([ ancilla_size + _ for _ in range(n_qubits)],[_ for _ in range(n_qubits)])

    job = execute(circuit,backend = Aer.get_backend('qasm_simulator'),shots = n_shots,optimization_level = 3).result().get_counts()
    stv = []
    for i in range(2**n_qubits):
        if convert_bin(i,n_qubits) in job.keys():
            stv.append(job[convert_bin(i,n_qubits)])
        else :
            stv.append(0)
    stv = np.array(stv)
    stv = stv/np.linalg.norm(stv)

    b = np.ones(2**n_qubits)
    b = b/np.linalg.norm(b)

    cost = np.matmul(np.matrix.getH(b),stv)

    return(cost)

def cost_fn_qiskit(weights):

    global Cost_hist

    p_gg = global_ground_qiskit(weights)
    Cost_hist.append(1 - p_gg)
    return(1 - p_gg)
```

```python
def global_ground_qiskit_tket(weights):

    num_parameters = len(keys) * n_qubits
    parameters = []
    for i in range(num_parameters):
        parameters.append(Parameter('w_' + str(i)))

    weights = weights
    value_dict = {}
    for i in range(len(parameters)) :
        value_dict[parameters[i]] = float(weights[i])

    parameters = np.array(parameters).reshape(len(keys),n_qubits)

    circuit = QuantumCircuit(tot_qubits,n_qubits)
    circuit.append(full_circuit(parameters),[_ for _ in range(tot_qubits)])
    circuit = circuit.bind_parameters(value_dict)
    circuit.measure([ ancilla_size + _ for _ in range(n_qubits)],[_ for _ in range(n_qubits)])

    #############################
    #############################
    # compiled by tket

    tket_back = tket_AerBackend()

    # # Normal TKET compilation and running on Tket wrapped AERbackend
    # tket_back = tket_AerBackend()
    # circuit = qiskit_to_tk(circuit)
    # tket_back.compile_circuit(circuit)
    # job = tket_back.get_counts(circuit, n_shots)
    # job = convert_format_to_qiskit(job)

    # # DecomposeMultiQubitsIBM()
     #pass1 = DecomposeMultiQubitsIBM()
     #cu = qiskit_to_tk(circuit)
     #cu = CompilationUnit(cu)
    #pass1.apply(cu)
     #circ1 = cu.circuit
     #tket_back = tket_AerBackend()
     #tket_back.compile_circuit(circ1)
     #job = tket_back.get_counts(circ1, n_shots)
     #job = convert_format_to_qiskit(job)

    # FullPeepholeOptimise
    #pass1 = FullPeepholeOptimise()
    #cu = qiskit_to_tk(circuit)
    #cu = CompilationUnit(cu)
    #pass1.apply(cu)
    #circ1 = cu.circuit
    #tket_back = tket_AerBackend()
    #tket_back.compile_circuit(circ1)
    #job = tket_back.get_counts(circ1, n_shots)
    #job = convert_format_to_qiskit(job)

    # # O2Pass
    # pass1 = O2Pass()
    # cu = qiskit_to_tk(circuit)
    # cu = CompilationUnit(cu)
    # pass1.apply(cu)
    # circ1 = cu.circuit
    # tket_back = tket_AerBackend()
    # tket_back.compile_circuit(circ1)
    # job = tket_back.get_counts(circ1, n_shots)
    # job = convert_format_to_qiskit(job)

    # # RebaseIBM
    # pass1 = RebaseIBM()
    # cu = qiskit_to_tk(circuit)
    # cu = CompilationUnit(cu)
    # pass1.apply(cu)
    # circ1 = cu.circuit
    # tket_back = tket_AerBackend()
    # tket_back.compile_circuit(circ1)
    # job = tket_back.get_counts(circ1, n_shots)
    # job = convert_format_to_qiskit(job)

    # # RemoveRedundancies
    # pass1 = RemoveRedundancies()
    # cu = qiskit_to_tk(circuit)
    # cu = CompilationUnit(cu)
    # pass1.apply(cu)
    # circ1 = cu.circuit
    # tket_back = tket_AerBackend()
    # tket_back.compile_circuit(circ1)
    # job = tket_back.get_counts(circ1, n_shots)
    # job = convert_format_to_qiskit(job)

    # # USquashIBM
    # pass1 = USquashIBM()
    # cu = qiskit_to_tk(circuit)
    # cu = CompilationUnit(cu)
    # pass1.apply(cu)
    # circ1 = cu.circuit
    # tket_back = tket_AerBackend()
    # tket_back.compile_circuit(circ1)
    # job = tket_back.get_counts(circ1, n_shots)
    # job = convert_format_to_qiskit(job)

    # # OptimisePhaseGadgets
    # pass1 = OptimisePhaseGadgets()
    # cu = qiskit_to_tk(circuit)
    # cu = CompilationUnit(cu)
    # pass1.apply(cu)
    # circ1 = cu.circuit
    # tket_back = tket_AerBackend()
    # tket_back.compile_circuit(circ1)
    # job = tket_back.get_counts(circ1, n_shots)
    # job = convert_format_to_qiskit(job)

    # # SequencePass
    seqpass = SequencePass([DecomposeMultiQubitsIBM(), FullPeepholeOptimise(), RemoveRedundancies(), USquashIBM()])
    reppass = RepeatPass(seqpass)
    cu = qiskit_to_tk(circuit)
    cu = CompilationUnit(cu)
    reppass.apply(cu)
    circ1 = cu.circuit
    tket_back = tket_AerBackend()
    tket_back.compile_circuit(circ1)
    job = tket_back.get_counts(circ1, n_shots)
    job = convert_format_to_qiskit(job)

    tket_back.compile_circuit(circ1)
    job = tket_back.get_counts(circ1, n_shots)
    job = convert_format_to_qiskit(job)

    ##############################
    ##############################

    job = execute(circuit,backend = Aer.get_backend('qasm_simulator'),shots = n_shots,optimization_level = 3).result().get_counts()
    stv = []
    for i in range(2**n_qubits):
        if convert_bin(i,n_qubits) in job.keys():
            stv.append(job[convert_bin(i,n_qubits)])
        else :
            stv.append(0)
    stv = np.array(stv)
    stv = stv/np.linalg.norm(stv)

    b = np.ones(2**n_qubits)
    b = b/np.linalg.norm(b)

    cost = np.matmul(np.matrix.getH(b),stv)

    return(cost)

def cost_fn_qiskit_tket(weights):

    global Cost_hist

    p_gg = global_ground_qiskit_tket(weights)
    Cost_hist.append(1 - p_gg)
    return(1 - p_gg)
```

```python
num_parameters = len(keys) * n_qubits
parameters = []
for i in range(num_parameters):
    parameters.append(Parameter('w_' + str(i)))

parameters = np.array(parameters).reshape(len(keys),n_qubits)
print('Circuit for qiskit-tket')
FC = full_circuit(parameters)

tket_back = tket_AerBackend()

FC_tket = qiskit_to_tk(FC)

tket_back.compile_circuit(FC_tket)

Graph(FC_tket).get_DAG()
```

    Circuit for qiskit-tket

```python
opt = qiskit.aqua.components.optimizers.COBYLA(maxiter=100, disp=False, rhobeg=1.0, tol=1e-8)
```

```python
weights = np.random.rand(num_parameters)
start = time.time()

Cost_hist = []
w = opt.optimize(num_vars = num_parameters , objective_function = cost_fn_qiskit_tket , gradient_function=None,
                 variable_bounds=[(0,2*np.pi) for i in range(num_parameters)], initial_point=weights)
stop = time.time()

Cost_hist_cobyla_qiskit_tket = np.copy(Cost_hist)
Cost_hist = []

print('Total time taken : ' , stop-start)
print('Optimized Cost : ', cost_fn_qiskit_tket(w[0]))
```

```python
print('Optimized Parameters :  ',w[0])
```

```python
plt.plot(Cost_hist_cobyla_qiskit_tket,'r-')
plt.xlabel('Number of times Q.Hardware accessed')
plt.ylabel('Cost')
plt.grid()
plt.show()
print(len(Cost_hist_cobyla_qiskit_tket))
```

```python
def prepare_x(weights_final):

    num_parameters = len(keys) * n_qubits
    parameters = []
    for i in range(num_parameters):
        parameters.append(Parameter('w_' + str(i)))

    value_dict = {}
    for i in range(len(parameters)) :
        value_dict[parameters[i]] = weights[i]

    parameters = np.array(parameters).reshape(len(keys),n_qubits)

    QR = QuantumRegister(tot_qubits)
    CR = ClassicalRegister(n_qubits)
    prep_circuit = QuantumCircuit(QR,CR)

    prep_circuit.append(UC(),[_ for _ in range(ancilla_size)])
    prep_circuit.barrier()
    prep_circuit.append(var_circuit(parameters) , [_ for _ in range(tot_qubits)])

    #prep_circuit.append(UCD(),[_ for _ in range(ancilla_size)])

    prep_circuit = prep_circuit.bind_parameters(value_dict)

    prep_circuit.measure([_ for _ in range(ancilla_size,tot_qubits)],[_ for _ in range(n_qubits)])

    prep_circuit = compiler.transpile(prep_circuit , basis_gates = ['u3','cx'],optimization_level = 3)

    #############################
    #############################
    # compiled buy tket and fetched back to qiskit

    tket_back = tket_AerBackend()

    #prep_circuit = qiskit_to_tk(prep_circuit)
    seqpass = SequencePass([DecomposeMultiQubitsIBM(), FullPeepholeOptimise(), RemoveRedundancies(), USquashIBM()])
    reppass = RepeatPass(seqpass)
    cu = qiskit_to_tk(prep_circuit)
    cu = CompilationUnit(cu)
    reppass.apply(cu)
    circ1 = cu.circuit
    tket_back = tket_AerBackend()
    tket_back.compile_circuit(circ1)
    job = tket_back.get_counts(circ1, n_shots)
    job = convert_format_to_qiskit(job)

    tket_back.compile_circuit(circ1)
    job = tket_back.get_counts(circ1, n_shots)
    job = convert_format_to_qiskit(job)

    ##############################
    ##############################

   # job = execute(prep_circuit , backend = Aer.get_backend('qasm_simulator') , shots = n_shots).result().get_counts()
    x = []
    for i in range(2**n_qubits):
        if convert_bin(i,n_qubits) in job.keys():
            x.append(job[convert_bin(i,n_qubits)])
        else :
            x.append(0)
    x = np.array(x)
    x = x/np.linalg.norm(x)
    print(job)

    return(x)
```

```python
weights = w[0]
 ## chage this with optimised weights
X = prepare_x(weights)
print("The solution vector is : " , X)
```

```python
b =  [1 for _ in range(8) ]
b = b/np.linalg.norm(b)
x = np.matmul(np.linalg.inv(A),b)
c_probs = (x/np.linalg.norm(x))

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 4))

ax1.bar(np.arange(0, 2 ** n_qubits), c_probs, color="blue")
ax1.set_xlim(-0.5, 2 ** n_qubits - 0.5)
ax1.set_xlabel("Vector space basis")
ax1.set_title("Classical probabilities")

ax2.bar(np.arange(0, 2 ** n_qubits), X, color="green")
ax2.set_xlim(-0.5, 2 ** n_qubits - 0.5)
ax2.set_xlabel("Hilbert space basis")
ax2.set_title("Quantum probabilities")

plt.show()

print('Distance between classcal value and Quantum Calculated value : ',np.linalg.norm(X - c_probs))
print('Accuracy in percentage is ',(1-np.linalg.norm(X - c_probs))*100 ,'%')
```