Conversation
pollockDeVis
commented
Mar 20, 2026
- Major integration with Borg MOEA besides eNSGA-II from platypus
- connectors for Serial, Manager Worker, and Multi Manager Borg written in C with EMA Workbench and Platypus
- Different configurations for agents during optimization
- New Visualizations
…e unncessary code
… hpc with borg. getting some issues
…ed analysis.py to run optimization and put it in root directory
There was a problem hiding this comment.
Pull request overview
This PR introduces a major integration of the C-based Borg MOEA into the existing EMA-Workbench/Platypus optimization workflow, adds MOMADPS (multi-agent) optimization utilities, and expands post-processing/visualization tooling.
Changes:
- Add Borg C-wrapper (
solvers/moea/borg.py) and a PlatypusAlgorithmadapter (solvers/moea/borg_platypus_adapter.py) with serial/MS/MM execution paths. - Add new optimization orchestration for EMODPS/MOMADPS (incl. single-agent reruns) and supporting region/macro-region mapping utilities.
- Add postprocessing utilities (regret processing, Pareto–Nash extraction, feature importance) plus docs/assets/config updates.
Reviewed changes
Copilot reviewed 26 out of 31 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
solvers/moea/borg_platypus_adapter.py |
Platypus adapter around the Borg C wrapper + EMA context + runtime snapshot packaging helpers. |
solvers/moea/borg.py |
ctypes-based Borg MOEA wrapper with serial and MPI entrypoints. |
run_optimization.py |
New top-level orchestration for EMODPS + MOMADPS + single-agent runs using Borg or EpsNSGAII. |
pyproject.toml |
Version/python requirement bumps and dependency updates. |
justice/welfare/social_welfare_function.py |
Adds spatially-disaggregated welfare computation used by macro-agent objectives. |
justice/util/regional_configuration.py |
Adds macro-region mapping + macro aggregation utilities. |
justice/util/reevaluate_optimal_policy.py |
Adds a script to reevaluate selected optimal policies across scenarios and extract variables. |
justice/util/postprocessing_for_regret_calculations.py |
Adds a script for parallel scenario postprocessing and mapping generation. |
justice/util/pareto_nash_run.py |
Adds payoff-table generation and Pareto–Nash set extraction utilities. |
justice/util/output_data_processor.py |
Enhances reevaluation/extraction utilities with output dir creation + richer file naming. |
justice/util/feature_importance.py |
Adds CatBoost+SHAP feature-importance pipeline over regret/optimization outputs. |
justice/util/enumerations.py |
Updates optimizer enum to distinguish MS/MM Borg variants. |
justice/util/borg_archive_processor.py |
Adds CLI tool to package per-island intermediate snapshots into EMA-like tarballs. |
justice/util/EMA_model_wrapper.py |
Extends wrappers to MOMADPS and single-agent MOMADPS + macro aggregation support. |
justice/model.py |
Updates clustering file reference and formatting cleanup. |
docs/rl_docs.md |
Adds RL usage documentation. |
docs/diagrams/JUSTICE.svg |
Adds an SVG diagram for the README/docs. |
data/input/region_to_macro_5.npy |
Adds precomputed region→macro mapping asset. |
data/input/R5_regions.json |
Replaces/updates 5-region macro definitions used by mapping. |
data/input/R10_regions.json |
Adds 10-region mapping definition. |
analysis/normative_uncertainty_optimization.json |
Updates epsilons and reorders reference scenario key. |
analysis/momadps_config.json |
Adds MOMADPS configuration file. |
analysis/hpc_slurm_scripts/run_single_agent_array.sh |
Adds SLURM array script for single-agent runs. |
analysis/hpc_slurm_scripts/mm_justice.sh |
Adds SLURM script for MM Borg runs. |
analysis/hpc_slurm_scripts/mm_borg_exclusive.sh |
Adds an exclusive-node SLURM script for MM Borg runs. |
analysis/hpc_run.py |
Updates HPC entrypoint to run single-agent MOMADPS via new runner. |
analysis/analyzer.py |
Removes legacy analyzer module in favor of new runner(s). |
README.md |
Updates diagram embedding and replaces install/run text with citation block. |
CITATION.cff |
Adds citation metadata file. |
.gitignore |
Ignores generated plots/figures and local Borg shared libraries. |
Comments suppressed due to low confidence (2)
pyproject.toml:24
dependenciesdeclares numpy twice (numpy (>=1.23)andnumpy (==1.24.3)), which is an unsatisfiable/confusing constraint for resolvers and can lead to non-deterministic installs. Keep a single constraint (pin or range) and remove the duplicate entry.
"numpy (>=1.23)",
"scipy (>=1.10.0)",
"deap (>=1.4.0)",
"ipyparallel (>=8.6.1)",
"plotly (>=5.19.0)",
"pycountry (>=24.6.1)",
"seaborn (>=0.13.0)",
"tyro (>=0.9.24)",
"wandb (==0.20.1)",
"numpy (==1.24.3)",
"torch (==2.4.1)",
solvers/moea/borg.py:1078
Configuration.initialize()runs at import time, butinitialize()callsseed(), which callsConfiguration.check()and will raise ifsetBorgLibrary()failed to auto-detect the shared library. This makes importingsolvers.moea.borgfail on machines without the Borg .so/.dylib present, preventing even serial users from handling the error gracefully. Consider removing the import-time initialization and requiring explicit initialization (or makeinitialize()/seed()conditional onlibborgbeing successfully loaded).
Configuration.initialize()
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from borg import Borg, Configuration | ||
|
|
||
| if self.borg_library_path: | ||
| Configuration.setBorgLibrary(self.borg_library_path) |
There was a problem hiding this comment.
This adapter imports Borg/Configuration from the top-level module name borg, but the wrapper added in this PR lives at solvers/moea/borg.py and other call sites import it via from solvers.moea.borg import .... Using from borg import ... will fail unless a separate borg package is installed / on PYTHONPATH. Update the import to use the in-repo module path (or a relative import) to avoid ModuleNotFoundError and ambiguity.
| if __name__ == "__main__": | ||
|
|
||
| # Get swf, ssp, base_dir from sys.argv or set default values | ||
| base_dir = sys.argv[1] if len(sys.argv) > 4 else "data/temporary/NU_DATA/mmBorg/" |
There was a problem hiding this comment.
base_dir = sys.argv[1] if len(sys.argv) > 4 else ... uses the wrong length check. When 2–4 CLI args are provided, this falls back to the default but still tries to read sys.argv[2]/sys.argv[3] below, and when exactly 2 args are provided it will IndexError on sys.argv[1] if the condition is incorrect. The check for sys.argv[1] should be len(sys.argv) > 1 (and similarly for other indices).
| base_dir = sys.argv[1] if len(sys.argv) > 4 else "data/temporary/NU_DATA/mmBorg/" | |
| base_dir = sys.argv[1] if len(sys.argv) > 1 else "data/temporary/NU_DATA/mmBorg/" |
| def extract_if_needed(archive_path: Path) -> Path: | ||
| """If archive_path is a zip file, extract it to a temp dir and return the mm_intermediate folder. | ||
| Otherwise, assume it already points to the mm_intermediate directory.""" | ||
| if archive_path.is_file() and archive_path.suffix == ".zip": | ||
| temp_dir = Path(tempfile.mkdtemp(prefix="mm_intermediate_")) | ||
| with zipfile.ZipFile(archive_path, "r") as zf: | ||
| zf.extractall(temp_dir) | ||
| subdirs = [p for p in temp_dir.glob("*") if p.is_dir()] | ||
| if len(subdirs) == 1: | ||
| extracted_root = subdirs[0] | ||
| else: | ||
| extracted_root = temp_dir / "mm_intermediate" | ||
| return extracted_root | ||
| else: |
There was a problem hiding this comment.
extract_if_needed() extracts a zip archive to a temp directory created via tempfile.mkdtemp(...) but never cleans it up, leaving temporary folders behind on every run. Prefer using tempfile.TemporaryDirectory() and cleaning up after packaging, or document that callers must remove the extracted directory.
| _ArchiveView, | ||
| _AlgorithmStub, | ||
| _create_intermediate_archives, | ||
| ) |
There was a problem hiding this comment.
run_optimization.py imports underscored names (_ArchiveView, _AlgorithmStub, _create_intermediate_archives) from borg_platypus_adapter. Since underscore-prefixed symbols are intended as private implementation details, this creates a fragile dependency between modules. Consider either (a) making these helpers part of the adapter's public API (drop underscores / add __all__) or (b) moving this runtime/archive logic into run_optimization.py (or a dedicated public helper module).
| _ArchiveView, | |
| _AlgorithmStub, | |
| _create_intermediate_archives, | |
| ) | |
| ) | |
| import solvers.moea.borg_platypus_adapter as _borg_platypus_adapter | |
| _ArchiveView = _borg_platypus_adapter._ArchiveView | |
| _AlgorithmStub = _borg_platypus_adapter._AlgorithmStub | |
| _create_intermediate_archives = _borg_platypus_adapter._create_intermediate_archives |
| import matplotlib.pyplot as plt | ||
|
|
||
| from catboost import CatBoostRegressor, Pool | ||
| from sklearn.model_selection import KFold | ||
|
|
There was a problem hiding this comment.
This module imports catboost, sklearn, and matplotlib at import time, but these packages are not listed in pyproject.toml dependencies. As a result, installing the project will succeed but importing justice.util.feature_importance will raise ImportError in many environments. Either add these to dependencies (or an optional extra) or move the imports inside the functions/CLI entrypoints that require them.
| import matplotlib.pyplot as plt | |
| from catboost import CatBoostRegressor, Pool | |
| from sklearn.model_selection import KFold | |
| class _MissingDependency: | |
| """ | |
| Lightweight proxy used when optional dependencies are not installed. | |
| Any attribute access will raise an informative ImportError. | |
| """ | |
| def __init__(self, package: str, feature: str = "this functionality") -> None: | |
| self._package = package | |
| self._feature = feature | |
| def __getattr__(self, name): | |
| raise ImportError( | |
| f"{self._package} is required to use {self._feature} " | |
| f"in justice.util.feature_importance but is not installed." | |
| ) | |
| def _missing_dependency_callable(package: str, feature: str): | |
| """ | |
| Return a callable that raises ImportError when invoked. | |
| Used for missing classes/functions such as CatBoostRegressor, Pool, or KFold. | |
| """ | |
| def _raiser(*args, **kwargs): | |
| raise ImportError( | |
| f"{package} is required to use {feature} " | |
| f"in justice.util.feature_importance but is not installed." | |
| ) | |
| return _raiser | |
| try: | |
| import matplotlib.pyplot as plt | |
| except ImportError: # pragma: no cover | |
| plt = _MissingDependency("matplotlib", "plotting utilities") # type: ignore | |
| try: | |
| from catboost import CatBoostRegressor, Pool | |
| except ImportError: # pragma: no cover | |
| CatBoostRegressor = _missing_dependency_callable("catboost", "CatBoostRegressor") # type: ignore | |
| Pool = _missing_dependency_callable("catboost", "Pool") # type: ignore | |
| try: | |
| from sklearn.model_selection import KFold | |
| except ImportError: # pragma: no cover | |
| KFold = _missing_dependency_callable("scikit-learn", "KFold") # type: ignore |
| start_year = 2015 | ||
| end_year = 2300 | ||
| data_timestep = 5 | ||
| timestep = 1 | ||
|
|
There was a problem hiding this comment.
The function parameters start_year, end_year, data_timestep, and timestep are immediately overwritten with hard-coded values, so caller-provided values are ignored. This makes the function signature misleading and can cause unexpected behavior when used outside the default 2015–2300 configuration. Either remove these parameters or stop overwriting them and rely on the passed-in values (or the config file) consistently.
| start_year = 2015 | |
| end_year = 2300 | |
| data_timestep = 5 | |
| timestep = 1 |
| - deep uncertainty | ||
| - normative uncertainty | ||
| license: BSD-3-Clause | ||
| version: 0.2.0 |
There was a problem hiding this comment.
CITATION.cff still lists version: 0.2.0 while pyproject.toml bumps the project to 1.0.0. This inconsistency can confuse downstream citation tooling and users. Update the citation metadata version (and release date/DOI if applicable) to match the package version.
| version: 0.2.0 | |
| version: 1.0.0 |