Skip to content

universal waste handling enabled#18

Open
purboday wants to merge 1 commit intortn-integration-phase-2from
universal-waste-handling
Open

universal waste handling enabled#18
purboday wants to merge 1 commit intortn-integration-phase-2from
universal-waste-handling

Conversation

@purboday
Copy link
Collaborator

@purboday purboday commented Feb 16, 2026

This PR extends the ABM circular economy model to support universal waste regulations and infrastructure, enabling differentiated waste routing based on regulatory classification (hazardous, universal waste, or regular waste).

Changes

  • Added storage limits in days and conversion logic for different time steps.
  • Added support for loading universal waste-specific facilities:
    • Universal_Waste_Landfills_data.csv - dedicated landfill sites
    • Universal_Waste_Recyclers_data.csv - certified recycling facilities
  • Consumer agents now route waste to appropriate facilities based on regulatory classification. Added universal waste-specific transportation cost and distance tracking.
  • Enhanced TCLP test logic to distinguish between hazardous and universal waste.
  • Implemented storage time limits for universal waste. Added universal waste generator size classification based on annual waste generation.
  • Created helper functions to identify closest recycler facilities

@@ -1,9 +1,9 @@
generator_size,max_storage_kg,max_storage_years,waste_generation_limit_kg,state
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changed the storage time to use days in line with the state regulations.

@@ -0,0 +1,5 @@
generator_size,max_storage_kg,max_storage_days,waste_generation_limit_kg,state
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Currently this is only used to set the storage time limit for universal waste.

Okon Recycling,Texas,Dallas,32.779167,-96.808891,FALSE
Powerhouse Recycling,North Carolina,Salisbury,35.671,-80.4742,FALSE
FabTech Solar Solutions,Arizona,Gilbert,33.352763,-111.78904,FALSE No newline at end of file
Recycler Name,State,City,Latitude,Longitude,RCRA permit,Universal Waste Permit
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added a new column for UW to distinguish between recycling facilities.

@@ -0,0 +1,58 @@
,Facility Name,Longitude,Latitude,$/ Ton
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added UW landfills in a separate input file

Copy link
Collaborator

@jwalzberg jwalzberg Feb 25, 2026

Choose a reason for hiding this comment

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

If I understand correctly the list of hazardous landfills is "Landfill_data_SA.csv" and costs are $175/ton and this new file has a longer list of UW landfills with costs of $114/ton. I can't remember what is the source for this new dataset (both list and associated costs) but we will have to document.

@@ -0,0 +1,9 @@
Recycler Name,State,City,Longitude,Latitude,RCRA permit,Universal Waste Permit
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added UW recyclers in a separate input file

Copy link
Collaborator

Choose a reason for hiding this comment

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

Same comment as for the UW landfill file, we will need to document source of those.


self.update_product_storage_hazardous()
if self.is_hazardous_waste_storage_limit_exceeded():
if self.is_hazardous_waste_storage_limit_exceeded() or \
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Since UW is considered as a subset of hazardous waste, it shares the tracking variables.

# if not universal_waste_thresholds:
# # If no thresholds are provided, default to SMALL generator size
# self.generator_size = GeneratorSize.SMALL
past_year_index = len(self.new_products_hard_copy) - self.model.timestep.value
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Gets the past 1 year's waste depending on the time step selected.

Copy link
Collaborator

@jwalzberg jwalzberg Feb 25, 2026

Choose a reason for hiding this comment

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

I am not sure why self.new_products_hard_copy is being used... I think this variable is a list of installed capacity in W at each time step? It's not the amount of waste stored... why not using a variable linked to the waste generated at each time step? I think this method needs to be revised. Were we using the same logic for Hazardous waste? If so it would also need to be changed.

Looking at the update_universal_waste_generator_size method just above this one. I understand the logic and I think it works well. I am not sure why the logic is so different for update_universal_waste_generator_size. Can't we use the same as for regular hazardous waste?

# based on the TCLP test results.
self.hazardous = self.model.tclp_test()
self.tclp_test_result = int(self.hazardous)
is_tclp_positive = self.model.tclp_test()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We do the TCLP first and if it is hazardous, then we check for universal waste.

if regulator_thresholds[self.generator_size].max_storage_kg is not None:
self.max_storage_hazardous_kg = regulator_thresholds[self.generator_size].max_storage_kg

def _find_closest_recycler_name(self, distance_df):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Used to get the recycler name and the agent id based on the distance. Currently it is used to update the recycler name in case of universal waste.


return df_expanded

def get_number_of_days_in_timestep(timestep: TIMESTEP) -> int:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is used to calculate the storage time limit based on the time step selected. For example using Quarterly time step: 180 days / 90 =2 days = 2 time steps.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor comment: 180 days / 90 days per time step = 2 time steps

@purboday purboday requested a review from jwalzberg February 17, 2026 00:39
@jwalzberg
Copy link
Collaborator

@purboday, I will work on this review on Tuesday.

@@ -996,7 +1009,7 @@ def update_product_storage_hazardous(self):
"""
Update the storage of hazardous products based on the purchase choice.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor edit: I think "based on the purchase choice" should be "based on the EoL choice"

return True
return False

def is_universal_waste_storage_limit_exceeded(self):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we combine both function (universal waste and hazardous waste limit exceeded checks?). For now let's keep as two separate functions for interest of time but we can revisit later.

"""
if self.hazardous:
return self.hazardous_landfill_cost + \
self.model.hazardous_waste_management_cost['landfill'] / 1E3 * self.model.dynamic_product_average_wght
Copy link
Collaborator

@jwalzberg jwalzberg Feb 25, 2026

Choose a reason for hiding this comment

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

self.model.hazardous_waste_management_cost['landfill'] is 0 right now. I imagine this is some sort of premium? We know that the total premium (including landfill fee premium and longer distances) should lead to 5-10 times higher total costs. In our notes we found that we needed to change transportation costs to $0.395/t.km instead of $0.095/t.km.

However, if we want to deal with the cost premium differently we could set the self.model.hazardous_waste_management_cost['landfill'] to $300/ton. I think that would be the best way to model the cost premium (i.e. leave transportation costs untouched at $0.095/t.km, use the hazardous and UW landfill fees we have from the csv files and add this cost premium of $300/ton). For UW this cost premium could be $150/ton instead (as we said we can assume a midlle ground value). This cost premium would represent permit costs, aditional skilled labor to handle hazardous waste, etc. for both UW and hazardous waste (with UW representing a lesser burden since permits and so on are easier to deal with).

To make those modifications we could create a new PR once this one is merged as is or just push a new commit before we merge.

return self.hazardous_landfill_cost + \
self.model.hazardous_waste_management_cost['landfill'] / 1E3 * self.model.dynamic_product_average_wght
elif self.universal_waste:
return self.universal_waste_landfill_cost
Copy link
Collaborator

Choose a reason for hiding this comment

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

Half the premium should be added here: self.universal_waste_landfill_cost + (self.model.hazardous_waste_management_cost['landfill'] / 2) / 1E3 * self.model.dynamic_product_average_wght

landfill_solar_waste_acceptance_ratio=0.4,
last_step=31,
sa_landfill_costs=(False, 0.0037),
file_name={'Landfill data': "Landfills_data_2023.csv",
Copy link
Collaborator

Choose a reason for hiding this comment

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

I can't find the Landfills_data_2023.csv in the TEMP file. Is it because we have it in the gitignore file (becaus eit is proprietary)?

if not hazardous_recycler_row.empty and hazardous_recycler_row['RCRA permit'].values[0]:
self.hazardous = True
# Check if the recycler is a universal waste recycler
universal_waste_recycler_row = self.model.recycler_data[self.model.recycler_data['Recycler Name'] == self.recycler_name]
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like it is using the same variable as for hazardous recyclers: self.model.recycler_data. Checking the ABM_CE_PV_Model.py file, the "Universal_Waste_Recyclers_data.csv" file seems to never be used.

Copy link
Collaborator

@jwalzberg jwalzberg left a comment

Choose a reason for hiding this comment

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

@purboday, this is really really good! I like all the new pieces that have beed added. I have left several comments that I think need to be addressed (or explained) before we can proceed to the merge though. We can meeto quickly to discuss them at any time although the RTN work is probably more important right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants