Skip to content

Commit

Permalink
Merge pull request #451 from NREL/develop
Browse files Browse the repository at this point in the history
October 2024: Fixes for battery dispatch and ASHP sizing, Update for GHP District
  • Loading branch information
Bill-Becker authored Oct 16, 2024
2 parents 151b102 + cdd713d commit ec0a146
Show file tree
Hide file tree
Showing 29 changed files with 210,641 additions and 111 deletions.
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Guidelines
- When making a Pull Request into `develop` start a new double-hash header for "Develop - YYYY-MM-DD"
- When working in feature branch, start a new double-hash header with the name of the branch and record changes under that
- When merging `develop` into a feature branch, keep the feature branch section and the "Develop" section separate to simplify merge conflicts
- When making a Pull Request into `develop`, merge the feature branch section into the "Develop" section (if it exists), else rename the feature branch header to "Develop"
- When making a Pull Request into `master` change "Develop" to the next version number

### Formatting
Expand All @@ -23,6 +25,24 @@ Classify the change according to the following categories:
### Deprecated
### Removed

## v0.48.1
### Changed
- Replace all `1/p.s.settings.time_steps_per_hour` with `p.hours_per_time_step` for simplicity/consistency
- Rename function `add_storage_sum_constraints` to `add_storage_sum_grid_constraints` for clarity
### Added
- Constraints to prevent simultaneous charge/discharge of storage
- Specify in docstrings that **PV** **max_kw** and **size_kw** are kW-DC
- Add the Logging package to `test/Project.toml` because it is used in `runtests.jl`
### Fixed
- Force **ElectricLoad** **critical_load_kw** to be _nothing_ when **off_grid_flag** is _true_ (**critical_load_fraction** was already being forced to 1, but the user was still able to get around this by providing **critical_load_kw**)
- Removed looping over storage name in functions `add_hot_thermal_storage_dispatch_constraints` and `add_cold_thermal_storage_dispatch_constraints` because this loop is already done when calling these functions and storage name is passed in as argument `b`
- Remove extraneous line of code in `results/wind.jl`
- Change type of **value_of_lost_load** in **FinancialInputs** struct to fix convert error when user provides an _Int_
- Change international location in "Solar Dataset" test set from Cameroon to Oulu because the locations in the NSRDB have been expanded significantly so there is now an NSRDB point at Cameroon
- Handle edge case where the values of **outage_start_time_steps** and **outage_durations** makes an outage extend beyond the end of the year. The outage will now wrap around to the beginning of the year.
- Enforce minimum allowable sizes for ASHP technologies by introducing improved big-M values for segmented size constraints.
- Removed default values from ASHP functions that calculate minimum allowable size and performance.

## v0.48.0
### Added
- Added new file `src/core/ASHP.jl` with new technology **ASHP**, which uses electricity as input and provides heating and/or cooling as output; load balancing and technology-specific constraints have been updated and added accordingly
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "REopt"
uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6"
authors = ["Nick Laws", "Hallie Dunham <[email protected]>", "Bill Becker <[email protected]>", "Bhavesh Rathod <[email protected]>", "Alex Zolan <[email protected]>", "Amanda Farthing <[email protected]>"]
version = "0.48.0"
version = "0.48.1"

[deps]
ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3"
Expand Down
18 changes: 13 additions & 5 deletions src/constraints/outage_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
function add_dv_UnservedLoad_constraints(m,p)
# Effective load balance
@constraint(m, [s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps, ts in p.s.electric_utility.outage_time_steps],
m[:dvUnservedLoad][s, tz, ts] == p.s.electric_load.critical_loads_kw[tz+ts-1]
- sum( m[:dvMGRatedProduction][t, s, tz, ts] * (p.production_factor[t, tz+ts-1] + p.unavailability[t][tz+ts-1]) * p.levelization_factor[t]
m[:dvUnservedLoad][s, tz, ts] == p.s.electric_load.critical_loads_kw[time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)]
- sum( m[:dvMGRatedProduction][t, s, tz, ts] * (p.production_factor[t, time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)] + p.unavailability[t][time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)]) * p.levelization_factor[t]
- m[:dvMGProductionToStorage][t, s, tz, ts] - m[:dvMGCurtail][t, s, tz, ts]
for t in p.techs.elec
)
Expand All @@ -22,7 +22,7 @@ end

function add_outage_cost_constraints(m,p)
@constraint(m, [s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps],
m[:dvMaxOutageCost][s] >= p.pwf_e * sum(p.value_of_lost_load_per_kwh[tz+ts-1] * m[:dvUnservedLoad][s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s])
m[:dvMaxOutageCost][s] >= p.pwf_e * sum(p.value_of_lost_load_per_kwh[time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)] * m[:dvUnservedLoad][s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s])
)

@expression(m, ExpectedOutageCost,
Expand Down Expand Up @@ -116,7 +116,7 @@ function add_MG_production_constraints(m,p)
# Electrical production sent to storage or export must be less than technology's rated production
@constraint(m, [t in p.techs.elec, s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps, ts in p.s.electric_utility.outage_time_steps],
m[:dvMGProductionToStorage][t, s, tz, ts] + m[:dvMGCurtail][t, s, tz, ts] <=
(p.production_factor[t, tz+ts-1] + p.unavailability[t][tz+ts-1]) * p.levelization_factor[t] * m[:dvMGRatedProduction][t, s, tz, ts]
(p.production_factor[t, time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)] + p.unavailability[t][time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)]) * p.levelization_factor[t] * m[:dvMGRatedProduction][t, s, tz, ts]
)

@constraint(m, [t in p.techs.elec, s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps, ts in p.s.electric_utility.outage_time_steps],
Expand All @@ -138,7 +138,7 @@ function add_MG_Gen_fuel_burn_constraints(m,p)
# Define dvMGFuelUsed by summing over outage time_steps.
@constraint(m, [t in p.techs.gen, s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps],
m[:dvMGFuelUsed][t, s, tz] == fuel_slope_gal_per_kwhe * p.hours_per_time_step * p.levelization_factor[t] *
sum( (p.production_factor[t, tz+ts-1] + p.unavailability[t][tz+ts-1]) * m[:dvMGRatedProduction][t, s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s])
sum( (p.production_factor[t, time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)] + p.unavailability[t][time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)]) * m[:dvMGRatedProduction][t, s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s])
+ fuel_intercept_gal_per_hr * p.hours_per_time_step *
sum( m[:binMGGenIsOnInTS][s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s])
)
Expand Down Expand Up @@ -287,6 +287,14 @@ function add_MG_storage_dispatch_constraints(m,p)
)
)

# Prevent simultaneous charge and discharge by limitting charging alone to not make the SOC exceed 100%
@constraint(m, [ts in p.time_steps_without_grid],
m[:dvStorageEnergy]["ElectricStorage"] >= m[:dvMGStoredEnergy][s, tz, ts-1] + p.hours_per_time_step * (
p.s.storage.attr["ElectricStorage"].charge_efficiency * sum(m[:dvMGProductionToStorage][t, s, tz, ts] for t in p.techs.elec)
)
)

# Min SOC
if p.s.storage.attr["ElectricStorage"].soc_min_applies_during_outages
# Minimum state of charge
@constraint(m, [s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps, ts in p.s.electric_utility.outage_time_steps],
Expand Down
70 changes: 50 additions & 20 deletions src/constraints/storage_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,35 @@ end

function add_elec_storage_dispatch_constraints(m, p, b; _n="")

# Constraint (4g): state-of-charge for electrical storage - with grid
# Constraint (4g)-1: state-of-charge for electrical storage - with grid
@constraint(m, [ts in p.time_steps_with_grid],
m[Symbol("dvStoredEnergy"*_n)][b, ts] == m[Symbol("dvStoredEnergy"*_n)][b, ts-1] + p.hours_per_time_step * (
sum(p.s.storage.attr[b].charge_efficiency * m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec)
+ p.s.storage.attr[b].grid_charge_efficiency * m[Symbol("dvGridToStorage"*_n)][b, ts]
- m[Symbol("dvDischargeFromStorage"*_n)][b,ts] / p.s.storage.attr[b].discharge_efficiency
)
)

# Constraint (4h): state-of-charge for electrical storage - no grid
# Constraint (4g)-2: state-of-charge for electrical storage - no grid
@constraint(m, [ts in p.time_steps_without_grid],
m[Symbol("dvStoredEnergy"*_n)][b, ts] == m[Symbol("dvStoredEnergy"*_n)][b, ts-1] + p.hours_per_time_step * (
sum(p.s.storage.attr[b].charge_efficiency * m[Symbol("dvProductionToStorage"*_n)][b,t,ts] for t in p.techs.elec)
- m[Symbol("dvDischargeFromStorage"*_n)][b, ts] / p.s.storage.attr[b].discharge_efficiency
)
)

# Constraint (4h): prevent simultaneous charge and discharge by limitting charging alone to not make the SOC exceed 100%
# (4h)-1: with grid
@constraint(m, [ts in p.time_steps_with_grid],
m[Symbol("dvStorageEnergy"*_n)][b] >= m[Symbol("dvStoredEnergy"*_n)][b, ts-1] + p.hours_per_time_step * (
sum(p.s.storage.attr[b].charge_efficiency * m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec)
+ p.s.storage.attr[b].grid_charge_efficiency * m[Symbol("dvGridToStorage"*_n)][b, ts]
)
)
# (4h)-2: no grid
@constraint(m, [ts in p.time_steps_without_grid],
m[Symbol("dvStorageEnergy"*_n)][b] >= m[Symbol("dvStoredEnergy"*_n)][b, ts-1] + p.hours_per_time_step * (
sum(p.s.storage.attr[b].charge_efficiency * m[Symbol("dvProductionToStorage"*_n)][b,t,ts] for t in p.techs.elec)
)
)

# Constraint (4i)-1: Dispatch to electrical storage is no greater than power capacity
Expand Down Expand Up @@ -104,16 +118,24 @@ end
function add_hot_thermal_storage_dispatch_constraints(m, p, b; _n="")

# Constraint (4j)-1: Reconcile state-of-charge for (hot) thermal storage
@constraint(m, [b in p.s.storage.types.hot, ts in p.time_steps],
m[Symbol("dvStoredEnergy"*_n)][b,ts] == m[Symbol("dvStoredEnergy"*_n)][b,ts-1] + (1/p.s.settings.time_steps_per_hour) * (
p.s.storage.attr[b].charge_efficiency * sum(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] for t in union(p.techs.heating, p.techs.chp), q in p.heating_loads) -
sum(m[Symbol("dvHeatFromStorage"*_n)][b,q,ts] for q in p.heating_loads) / p.s.storage.attr[b].discharge_efficiency -
p.s.storage.attr[b].thermal_decay_rate_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
@constraint(m, [ts in p.time_steps],
m[Symbol("dvStoredEnergy"*_n)][b,ts] == m[Symbol("dvStoredEnergy"*_n)][b,ts-1] + p.hours_per_time_step * (
p.s.storage.attr[b].charge_efficiency * sum(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] for t in union(p.techs.heating, p.techs.chp), q in p.heating_loads) -
sum(m[Symbol("dvHeatFromStorage"*_n)][b,q,ts] for q in p.heating_loads) / p.s.storage.attr[b].discharge_efficiency -
p.s.storage.attr[b].thermal_decay_rate_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
)
)

# Prevent simultaneous charge and discharge by limitting charging alone to not make the SOC exceed 100%
@constraint(m, [ts in p.time_steps],
m[Symbol("dvStorageEnergy"*_n)][b] >= m[Symbol("dvStoredEnergy"*_n)][b,ts-1] + p.hours_per_time_step * (
p.s.storage.attr[b].charge_efficiency * sum(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] for t in union(p.techs.heating, p.techs.chp), q in p.heating_loads)
- p.s.storage.attr[b].thermal_decay_rate_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
)
)

#Constraint (4n)-1: Dispatch to and from thermal storage is no greater than power capacity
@constraint(m, [b in p.s.storage.types.hot, ts in p.time_steps],
@constraint(m, [ts in p.time_steps],
m[Symbol("dvStoragePower"*_n)][b] >=
sum(m[Symbol("dvHeatFromStorage"*_n)][b,q,ts] +
sum(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] for t in union(p.techs.heating, p.techs.chp))
Expand All @@ -122,14 +144,14 @@ function add_hot_thermal_storage_dispatch_constraints(m, p, b; _n="")
# TODO missing thermal storage constraints from API ???

# Constraint (4o): Discharge from storage is equal to sum of heat from storage for all qualities
@constraint(m, HeatDischargeReconciliation[b in p.s.storage.types.hot, ts in p.time_steps],
@constraint(m, HeatDischargeReconciliation[ts in p.time_steps],
m[Symbol("dvDischargeFromStorage"*_n)][b,ts] ==
sum(m[Symbol("dvHeatFromStorage"*_n)][b,q,ts] for q in p.heating_loads)
)

#Do not allow GHP to charge storage
if !isempty(p.techs.ghp)
for b in p.s.storage.types.hot, t in p.techs.ghp, q in p.heating_loads, ts in p.time_steps
for t in p.techs.ghp, q in p.heating_loads, ts in p.time_steps
fix(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts], 0.0, force=true)
end
end
Expand All @@ -140,36 +162,44 @@ function add_cold_thermal_storage_dispatch_constraints(m, p, b; _n="")

# Constraint (4f)-2: (Cold) Thermal production sent to storage or grid must be less than technology's rated production
if !isempty(p.techs.cooling)
@constraint(m, CoolingTechProductionFlowCon[b in p.s.storage.types.cold, t in p.techs.cooling, ts in p.time_steps],
@constraint(m, CoolingTechProductionFlowCon[t in p.techs.cooling, ts in p.time_steps],
m[Symbol("dvProductionToStorage"*_n)][b,t,ts] <=
m[Symbol("dvCoolingProduction"*_n)][t,ts]
)
end

# Constraint (4j)-2: Reconcile state-of-charge for (cold) thermal storage
@constraint(m, ColdTESInventoryCon[b in p.s.storage.types.cold, ts in p.time_steps],
m[Symbol("dvStoredEnergy"*_n)][b,ts] == m[Symbol("dvStoredEnergy"*_n)][b,ts-1] + (1/p.s.settings.time_steps_per_hour) * (
sum(p.s.storage.attr[b].charge_efficiency * m[Symbol("dvProductionToStorage"*_n)][b,t,ts] for t in p.techs.cooling) -
m[Symbol("dvDischargeFromStorage"*_n)][b,ts]/p.s.storage.attr[b].discharge_efficiency -
p.s.storage.attr[b].thermal_decay_rate_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
@constraint(m, ColdTESInventoryCon[ts in p.time_steps],
m[Symbol("dvStoredEnergy"*_n)][b,ts] == m[Symbol("dvStoredEnergy"*_n)][b,ts-1] + p.hours_per_time_step * (
sum(p.s.storage.attr[b].charge_efficiency * m[Symbol("dvProductionToStorage"*_n)][b,t,ts] for t in p.techs.cooling) -
m[Symbol("dvDischargeFromStorage"*_n)][b,ts]/p.s.storage.attr[b].discharge_efficiency -
p.s.storage.attr[b].thermal_decay_rate_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
)
)

# Prevent simultaneous charge and discharge by limitting charging alone to not make the SOC exceed 100%
@constraint(m, [ts in p.time_steps],
m[Symbol("dvStorageEnergy"*_n)][b] >= m[Symbol("dvStoredEnergy"*_n)][b,ts-1] + p.hours_per_time_step * (
p.s.storage.attr[b].charge_efficiency * sum(m[Symbol("dvProductionToStorage"*_n)][b,t,ts] for t in p.techs.cooling)
- p.s.storage.attr[b].thermal_decay_rate_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
)
)

#Constraint (4n)-2: Dispatch to and from thermal storage is no greater than power capacity
@constraint(m, [b in p.s.storage.types.cold, ts in p.time_steps],
@constraint(m, [ts in p.time_steps],
m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts] +
sum(m[Symbol("dvProductionToStorage"*_n)][b,t,ts] for t in p.techs.cooling)
)

#Do not allow GHP to charge storage
if !isempty(p.techs.ghp)
for b in p.s.storage.types.cold, t in p.techs.ghp, ts in p.time_steps
for t in p.techs.ghp, ts in p.time_steps
fix(m[Symbol("dvProductionToStorage"*_n)][b,t,ts], 0.0, force=true)
end
end
end

function add_storage_sum_constraints(m, p; _n="")
function add_storage_sum_grid_constraints(m, p; _n="")

##Constraint (8c): Grid-to-storage no greater than grid purchases
@constraint(m, [ts in p.time_steps_with_grid],
Expand Down
2 changes: 1 addition & 1 deletion src/constraints/thermal_tech_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function add_boiler_tech_constraints(m, p; _n="")
)
if "Boiler" in p.techs.boiler # ExistingBoiler does not have om_cost_per_kwh
m[:TotalBoilerPerUnitProdOMCosts] = @expression(m, p.third_party_factor * p.pwf_om *
sum(p.s.boiler.om_cost_per_kwh / p.s.settings.time_steps_per_hour *
sum(p.s.boiler.om_cost_per_kwh * p.hours_per_time_step *
m[Symbol("dvHeatingProduction"*_n)]["Boiler",q,ts] for q in p.heating_loads, ts in p.time_steps)
)
else
Expand Down
10 changes: 5 additions & 5 deletions src/core/ashp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -444,14 +444,14 @@ function get_ashp_performance(cop_reference,
cf_reference,
reference_temps,
ambient_temp_degF,
back_up_temp_threshold_degF = 10.0
back_up_temp_threshold_degF
)
"""
function get_ashp_performance(cop_reference,
cf_reference,
reference_temps,
ambient_temp_degF,
back_up_temp_threshold_degF = 10.0
back_up_temp_threshold_degF
)
num_timesteps = length(ambient_temp_degF)
cop = zeros(num_timesteps)
Expand Down Expand Up @@ -493,9 +493,9 @@ Obtains the default minimum allowable size for ASHP system. This is calculated
"""
function get_ashp_default_min_allowable_size(heating_load::Array{<:Real,1},
heating_cf::Array{<:Real,1},
cooling_load::Array{<:Real,1} = Real[],
cooling_cf::Array{<:Real,1} = Real[],
peak_load_thermal_factor::Real = 0.5
cooling_load::Array{<:Real,1},
cooling_cf::Array{<:Real,1},
peak_load_thermal_factor::Real
)

if isempty(cooling_cf)
Expand Down
2 changes: 1 addition & 1 deletion src/core/bau_inputs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ function setup_bau_emissions_inputs(p::REoptInputs, s_bau::BAUScenario, generato
bau_grid_to_load = [max(i,0) for i in bau_grid_to_load]
end

bau_grid_emissions_lb_CO2_per_year = sum(p.s.electric_utility.emissions_factor_series_lb_CO2_per_kwh .* bau_grid_to_load) / p.s.settings.time_steps_per_hour
bau_grid_emissions_lb_CO2_per_year = sum(p.s.electric_utility.emissions_factor_series_lb_CO2_per_kwh .* bau_grid_to_load) * p.hours_per_time_step
bau_emissions_lb_CO2_per_year += bau_grid_emissions_lb_CO2_per_year

## Generator emissions (during outages)
Expand Down
12 changes: 9 additions & 3 deletions src/core/electric_load.jl
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,15 @@ mutable struct ElectricLoad # mutable to adjust (critical_)loads_kw based off o
min_load_met_annual_fraction::Real = off_grid_flag ? 0.99999 : 1.0 # if off grid, 99.999%, else must be 100%. Applied to each time_step as a % of electric load.
)

if off_grid_flag && !(critical_load_fraction == 1.0)
@warn "ElectricLoad critical_load_fraction must be 1.0 (100%) for off-grid scenarios. Any other value will be overriden when `off_grid_flag` is true. If you wish to alter the load profile or load met, adjust the loads_kw or min_load_met_annual_fraction."
critical_load_fraction = 1.0
if off_grid_flag
if !isnothing(critical_loads_kw)
@warn "ElectricLoad critical_loads_kw will be ignored because `off_grid_flag` is true. If you wish to alter the load profile or load met, adjust the loads_kw or min_load_met_annual_fraction."
critical_loads_kw = nothing
end
if critical_load_fraction != 1.0
@warn "ElectricLoad critical_load_fraction must be 1.0 (100%) for off-grid scenarios. Any other value will be overriden when `off_grid_flag` is true. If you wish to alter the load profile or load met, adjust the loads_kw or min_load_met_annual_fraction."
critical_load_fraction = 1.0
end
end

if !(off_grid_flag)
Expand Down
Loading

2 comments on commit ec0a146

@Bill-Becker
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

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

Registration pull request created: JuliaRegistries/General/117426

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.48.1 -m "<description of version>" ec0a146b963cfba956231567c83af3dce3e4e441
git push origin v0.48.1

Please sign in to comment.