Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ RUN apt-get -y update && apt-get install -y python3 git sudo vim python3-pip pyt

# Install the OMF
# Warning: clone might be cached. Consider invalidating manually.
RUN git clone --depth 1 https://github.com/dpinney/omf.git
RUN git clone --depth 1 https://github.com/nreca-bts/omf.git
RUN cd omf; sudo python3 install.py
# Install a compatible version of numpy<2.0.0
RUN pip install numpy==1.26.4
Expand Down
118 changes: 94 additions & 24 deletions microgridup_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,13 @@ def plot_inrush_data(dssPath, microgrids, out_html, outage_start, outage_end, ou
table_data['Microgrid ID'].append(key)

# # of Interruptions --> Number of times gen drops to zero during sim.
new_dss_tree, new_batt_loadshape, cumulative_existing_batt_shapes, all_rengen_shapes, total_surplus, all_loads_shapes_kw = do_manual_balance_approach(outage_start, outage_end, microgrids[key], dssTree, logger)
# new_dss_tree, new_batt_loadshape, cumulative_existing_batt_shapes, all_rengen_shapes, total_surplus, all_loads_shapes_kw = do_manual_balance_approach(outage_start, outage_end, microgrids[key], dssTree, logger)
compute_result = compute_manual_balance(outage_start, outage_end, microgrids[key], dssTree, logger)
new_dss_tree = apply_manual_balance_to_tree(compute_result, dssTree, logger) # Need the tree for transformer/load lists below
new_batt_loadshape = list(compute_result['new_batt_loadshape_sliced'])
cumulative_existing_batt_shapes = list(compute_result['cumulative_existing_batt_shapes'])
all_rengen_shapes = list(compute_result['all_rengen_shapes'])
all_loads_shapes_kw = compute_result['all_loads_shapes_kw']
# Stole a few lines from plot_manual_balance_approach().
new_batt_loadshape = list(new_batt_loadshape)
cumulative_existing_batt_shapes = list(cumulative_existing_batt_shapes)
Expand Down Expand Up @@ -421,18 +427,28 @@ def _parse_mult_field(dss_tree, mult_name):
return None
return None

def do_manual_balance_approach(outage_start, outage_end, mg_values, dssTree, logger):
def compute_manual_balance(outage_start, outage_end, mg_values, dssTree, logger):
'''
Manually constructs battery loadshapes for outage and reinserts into tree based on a proportional to kWh basis.
Returns: (new_dss_tree, new_batt_loadshape_sliced, cumulative_existing_batt_shapes,
all_rengen_shapes, total_surplus, all_loads_shapes_kw)
Given a dssTree (read-only), compute:
- per-hour net load (load - rengen)
- sliced outage battery loadshape (hours outage_start:outage_end)
- cumulative existing battery shapes (full-year)
- all_rengen_shapes (full-year)
- total_surplus list (per outage hour)
- all_loads_shapes_kw (full-year)
- batt_obj (list of battery objects found)
- batt_kwh, batt_kw (scalars)
- batt_obj_loadshape_names (list) to know which mult objects to replace later
No mutating dssTree
Returns a dict (compute_result)
'''
# Validate inputs
gen_bus = mg_values.get('gen_bus')
if not gen_bus:
msg = f"Manual balance: missing gen_bus in microgrid values: {mg_values}"
msg = f'Manual balance: missing gen_bus in microgrid values: {mg_values}'
logger.warning(msg)
raise ValueError(msg)
# First: collect loads and their yearly shapes
# Collect loads and their yearly shapes
all_mg_loads = [
ob for ob in dssTree
if ob.get('object') and ob.get('object').startswith('load.')
Expand All @@ -451,7 +467,7 @@ def do_manual_balance_approach(outage_start, outage_end, mg_values, dssTree, log
logger.warning(msg)
raise ValueError(msg)
load_shapes.append(arr)
# Ensure consistent length
# Ensure consistent length
lengths = {len(a) for a in load_shapes}
if len(lengths) != 1:
msg = f'Inconsistent loadshape lengths for microgrid {gen_bus}: lengths {lengths}'
Expand All @@ -476,18 +492,18 @@ def do_manual_balance_approach(outage_start, outage_end, mg_values, dssTree, log
all_rengen_shapes = np.sum(np.vstack(rengen_shapes), axis=0)
else:
all_rengen_shapes = np.zeros_like(all_loads_shapes_kw)
# Third: battery objects at gen_bus
# Battery objects at gen_bus
batt_obj = [
ob for ob in dssTree
if ob.get('bus1', 'x.x').split('.')[0] == gen_bus and 'storage' in ob.get('object', '')
]
# Keep deterministic ordering like original
# Deterministic ordering (small to large)
batt_obj.sort(key=lambda x: float(x.get('kwhrated', 0.0)))
batt_kws = [float(ob.get('kwrated', 0.0)) for ob in batt_obj]
batt_kwhs = [float(ob.get('kwhrated', 0.0)) for ob in batt_obj]
batt_kw = sum(batt_kws)
batt_kwh = sum(batt_kwhs)
# Net (load - rengen)
# Net (load - rengen) (full-year)
new_batt_loadshape = all_loads_shapes_kw - all_rengen_shapes
# Validate outage window
if outage_start < 0 or outage_end > len(new_batt_loadshape) or outage_end <= outage_start:
Expand All @@ -497,14 +513,16 @@ def do_manual_balance_approach(outage_start, outage_end, mg_values, dssTree, log
new_batt_loadshape_sliced = new_batt_loadshape[outage_start:outage_end].astype(float)
# Cumulative existing battery shapes
cumulative_existing_batt_shapes = np.zeros(len(new_batt_loadshape))
batt_obj_loadshape_names = []
for ob in batt_obj:
batt_shape_name = ob.get('yearly')
arr = _parse_mult_field(dssTree, batt_shape_name) if batt_shape_name else None
if arr is None:
logger.warning(f"Missing battery yearly shape for {ob.get('object')}; treating as zeros.")
arr = np.zeros_like(cumulative_existing_batt_shapes)
cumulative_existing_batt_shapes = cumulative_existing_batt_shapes + arr
# Starting capacity
batt_obj_loadshape_names.append(batt_shape_name)
# Starting capacity and battery simulation over outage slice
try:
starting_capacity = batt_kwh + float(np.sum(cumulative_existing_batt_shapes[:outage_start]))
except Exception:
Expand All @@ -521,7 +539,7 @@ def do_manual_balance_approach(outage_start, outage_end, mg_values, dssTree, log
except Exception:
batt_kwh = 0.0
battery_available_kwh = float(starting_capacity)
# Work on a copy
# Work on a copy of the outage slice
new_shape_arr = np.array(new_batt_loadshape_sliced, dtype=float)
for hour_idx in range(len(new_shape_arr)):
net_kw = new_shape_arr[hour_idx] # positive => net load; negative => surplus
Expand All @@ -545,12 +563,41 @@ def do_manual_balance_approach(outage_start, outage_end, mg_values, dssTree, log
if needed_kwh > actual_discharge_kwh:
unsupported_load_kwh += (needed_kwh - actual_discharge_kwh)
total_surplus.append(surplus_this_hour)
# Assign back
# Assign back final processed outage slice
new_batt_loadshape_sliced = new_shape_arr
# Build new dss tree shallow copy
# Build and return compute_result dict
compute_result = {
'gen_bus': gen_bus,
'outage_start': int(outage_start),
'outage_end': int(outage_end),
'new_batt_loadshape_sliced': new_batt_loadshape_sliced, # numpy array
'cumulative_existing_batt_shapes': cumulative_existing_batt_shapes, # numpy array
'all_rengen_shapes': all_rengen_shapes, # numpy array
'total_surplus': total_surplus, # list
'all_loads_shapes_kw': all_loads_shapes_kw, # numpy array
'batt_obj': batt_obj, # list of battery objects (dicts)
'batt_obj_loadshape_names': batt_obj_loadshape_names, # list of string names
'batt_kwh': batt_kwh,
'batt_kw': batt_kw
}
return compute_result

def apply_manual_balance_to_tree(compute_result, dssTree, logger):
'''
Take the compute_result (from compute_manual_balance) and return a new_dss_tree where battery 'mult' entries for batt_obj_loadshape_names have been patched to include the outage slice values computed in compute_manual_balance().
Does not modify the input dssTree in place — returns a shallow copy with the modified entries.
'''
outage_start = compute_result['outage_start']
outage_end = compute_result['outage_end']
new_batt_loadshape_sliced = compute_result['new_batt_loadshape_sliced']
batt_obj = compute_result['batt_obj']
batt_obj_loadshape_names = compute_result['batt_obj_loadshape_names']
batt_kwh = compute_result['batt_kwh']
# Shallow copy the tree (copy each dict)
new_dss_tree = [dict(ob) for ob in dssTree]
if batt_kwh > 0 and len(batt_obj) > 0:
for batt_ob in batt_obj:
# Iterate the batt_obj same order as compute()
for batt_ob, batt_shape_name in zip(batt_obj, batt_obj_loadshape_names):
try:
this_kwhr = float(batt_ob.get('kwhrated', 0.0))
this_kw = float(batt_ob.get('kwrated', 0.0))
Expand All @@ -559,18 +606,18 @@ def do_manual_balance_approach(outage_start, outage_end, mg_values, dssTree, log
per_unit_outage_shape = (new_batt_loadshape_sliced * factor) / this_kw
else:
per_unit_outage_shape = np.zeros_like(new_batt_loadshape_sliced)
batt_loadshape_name = batt_ob.get('yearly')
# Find the matching mult object index in new_dss_tree
found_index = None
for i, t in enumerate(new_dss_tree):
if t.get('object') and batt_loadshape_name in t.get('object'):
if t.get('object') and batt_shape_name in t.get('object'):
found_index = i
break
if found_index is None:
logger.warning(f"Could not find battery loadshape object for {batt_loadshape_name}; skipping replacement.")
logger.warning(f"Could not find battery loadshape object for {batt_shape_name}; skipping replacement.")
continue
full_loadshape_arr = _parse_mult_field(dssTree, batt_loadshape_name)
full_loadshape_arr = _parse_mult_field(dssTree, batt_shape_name)
if full_loadshape_arr is None:
logger.warning(f"Battery loadshape {batt_loadshape_name} missing or unparsable; skipping.")
logger.warning(f"Battery loadshape {batt_shape_name} missing or unparsable; skipping.")
continue
full_loadshape_list = full_loadshape_arr.tolist()
if outage_end > len(full_loadshape_list):
Expand All @@ -580,11 +627,27 @@ def do_manual_balance_approach(outage_start, outage_end, mg_values, dssTree, log
end_idx = outage_end
new_outage_vals = per_unit_outage_shape.tolist()
full_loadshape_list[outage_start:end_idx] = new_outage_vals[:end_idx - outage_start]
# Store back without spaces
new_dss_tree[found_index]['mult'] = str(full_loadshape_list).replace(' ', '')
except Exception as e:
logger.warning(f"Error updating battery loadshape for {batt_ob.get('object', '<unknown>')}: {e}")
# Final return (matching original signature)
return new_dss_tree, new_batt_loadshape_sliced, cumulative_existing_batt_shapes, all_rengen_shapes, total_surplus, all_loads_shapes_kw
return new_dss_tree

def do_manual_balance_approach(outage_start, outage_end, mg_values, dssTree, logger):
'''
Backwards-compatible wrapper: compute -> apply -> return tuple
Keeps callers unaffected while separating implementation
'''
compute_result = compute_manual_balance(outage_start, outage_end, mg_values, dssTree, logger)
new_dss_tree = apply_manual_balance_to_tree(compute_result, dssTree, logger)
return (
new_dss_tree,
compute_result['new_batt_loadshape_sliced'],
compute_result['cumulative_existing_batt_shapes'],
compute_result['all_rengen_shapes'],
compute_result['total_surplus'],
compute_result['all_loads_shapes_kw']
)

def plot_manual_balance_approach(mg_key, year, outage_start, outage_end, outage_length, new_batt_loadshape, cumulative_existing_batt_shapes, all_rengen_shapes, total_surplus, all_loads_shapes_kw):
# Creates plot of manually constructed battery activity, load, and renewable generation (all cumulative).
Expand Down Expand Up @@ -1650,7 +1713,14 @@ def play(data, outage_start, outage_length, logger):
else:
manual_balance_approach = True
# Manually construct battery loadshapes for outage.
new_dss_tree, new_batt_loadshape, cumulative_existing_batt_shapes, all_rengen_shapes, total_surplus, all_loads_shapes_kw = do_manual_balance_approach(outage_start, outage_end, mg, dssTree, logger)
# new_dss_tree, new_batt_loadshape, cumulative_existing_batt_shapes, all_rengen_shapes, total_surplus, all_loads_shapes_kw = do_manual_balance_approach(outage_start, outage_end, mg, dssTree, logger)
compute_result = compute_manual_balance(outage_start, outage_end, mg, dssTree, logger)
new_dss_tree = apply_manual_balance_to_tree(compute_result, dssTree, logger)
new_batt_loadshape = compute_result['new_batt_loadshape_sliced']
cumulative_existing_batt_shapes = compute_result['cumulative_existing_batt_shapes']
all_rengen_shapes = compute_result['all_rengen_shapes']
total_surplus = compute_result['total_surplus']
all_loads_shapes_kw = compute_result['all_loads_shapes_kw']
# Manual Balance Approach plotting call.
if manual_balance_approach == True:
fname = plot_manual_balance_approach(mg_name, 2019, outage_start, outage_end, outage_length, new_batt_loadshape, cumulative_existing_batt_shapes, all_rengen_shapes, total_surplus, all_loads_shapes_kw)
Expand Down
Loading