Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
8d0c3cb
DFL trustworthiness and numerical datasets
juanjetm Mar 4, 2026
87dd13a
Minor changes in trustworthiness
juanjetm Mar 6, 2026
2bcad93
KDDCUP99 dataset added and errors fixed
juanjetm Mar 11, 2026
d51f50b
Trustworthiness message system implemented
juanjetm Mar 12, 2026
77302f5
Minor changes in trustworthiness CFL
juanjetm Mar 12, 2026
39a6bfb
CFL trustworthiness emissions and dataresults done
juanjetm Mar 12, 2026
0ffa58e
All CFL files updated
juanjetm Mar 13, 2026
ed01c8f
CFL totally adapted
juanjetm Mar 16, 2026
7c6941e
Desynchronization error in CFL, accuracy/loss error and reports fixed
juanjetm Mar 17, 2026
1f4a1e4
Expected nodes fixed
juanjetm Mar 18, 2026
6c34549
DFL global trust score implemented with reputation
juanjetm Mar 25, 2026
fb7dccb
DFL and SDFL global trust scores added to Real-Time metrics
juanjetm Mar 25, 2026
91523c9
SDFL Trust malicious nodes fixed, frontend weights limit implemented …
juanjetm Mar 27, 2026
262f7f8
Names changed DFL trust
juanjetm Mar 27, 2026
f0f8129
Flooding and Delayer attacks fixed and minor changes
juanjetm Mar 27, 2026
49219b1
Trust logs updated
juanjetm Mar 27, 2026
f6deb02
Minor changes and new fairness and explainability metrics implemented
juanjetm Apr 8, 2026
66d39e9
All models updated, trustworthiness now accepts all models and datase…
juanjetm Apr 8, 2026
ea49fc7
Privacy metrics implemented: MIA AUC and Epsilon Star, all new metric…
juanjetm Apr 10, 2026
d2d7022
Some metrics changed and fixed, and macro F1, Reputation Enabled and …
juanjetm Apr 14, 2026
ee84a74
Dropout rate, timeout rate and topology type implemented, client sele…
juanjetm Apr 15, 2026
700383e
Accountability updated: Factsheet Completeness
juanjetm Apr 15, 2026
67d45ef
Aggregation algorithms updates, SDFL fixed
juanjetm Apr 17, 2026
a397a8a
New notions added: Federation Management and Monitoring, frontend upd…
juanjetm Apr 17, 2026
c073174
Notions: Dynamic weights fixed
juanjetm Apr 20, 2026
cb2dc94
Underffiting now uses validation, frontend fixed
juanjetm Apr 20, 2026
b5da1f5
Differential Privacy V1 implemented
juanjetm Apr 24, 2026
e813458
Differential Privacy V2, LightningDP implemented, frontend implemented
juanjetm Apr 24, 2026
8ac17cd
DP and epsilon added to trustworthiness
juanjetm Apr 24, 2026
b7b23c2
DP Trustworthiness CFL finished, global privacy risk fixed, frontend …
juanjetm Apr 27, 2026
134e6e2
CFL acc and loss fixed, DP changed
juanjetm Apr 27, 2026
13189ae
DP finished, Lightning fit
juanjetm Apr 28, 2026
62f5fda
Aggregation error fixed, trustworthiness refactorization (factsheet/C…
juanjetm Apr 28, 2026
28ccb01
Trustworthiness refactorization (factsheet/DFL), agnostic to model an…
juanjetm Apr 29, 2026
e204eaa
Trustworthiness refactor
juanjetm Apr 29, 2026
cc30ffb
DP error fixed DFL, calculation refactoring, maxgradnorm added to fro…
juanjetm Apr 30, 2026
32651fb
Factsheet refactoring, CFL reputation metrics added, CFL reputation u…
juanjetm May 4, 2026
5653780
SDFL Fixed: Forwarding trainer and aggregator, aggregation fixed, new…
juanjetm May 5, 2026
0126191
SDFL timeouts fixed, nodes accept global update after learning cycle,…
juanjetm May 6, 2026
eef6fef
Trustworthiness refactor: factsheets
juanjetm May 6, 2026
fee6990
Reputation implemented for SDFL. Leadership updated.
juanjetm May 11, 2026
d0c1f8d
Mitigation: Feature Squeezing for images
juanjetm May 14, 2026
ca88c85
Leadership updated: Leadership counter
juanjetm May 15, 2026
45b71f3
Metrics name changed
juanjetm May 15, 2026
328977f
Adversarial Training implemented for images, trustworthiness: FGSM fi…
juanjetm May 19, 2026
130ff34
Models updated: Data type, tabular datasets fixed and continous/binar…
juanjetm May 20, 2026
f3a4270
Trustworthiness: Tabular and images factsheet division, accuracy/stab…
juanjetm May 21, 2026
d2bd553
Eval_metrics updated and fixed. Weights updated
juanjetm May 21, 2026
08409dc
Adversarial training for tabular data (Adult Census), frontend update…
juanjetm May 25, 2026
3e1fd2b
Adversarial training implemented for CancerBreast, KDDCUP99 and Covty…
juanjetm May 25, 2026
98f0195
KDDCUP fixed, adversarial training working for KDD
juanjetm May 26, 2026
2153bcd
Global privacy risk fixed. Important SDFL refactoring.
juanjetm May 27, 2026
c0f5212
Adversarial training updated for tabular data: CAA V1 implemented
juanjetm May 28, 2026
ed8e608
Refactor: Feature Squeezing, factsheets, graphics and trustworthiness
juanjetm May 29, 2026
7516058
Refactoring calculation and utils
juanjetm May 29, 2026
23fec35
Refactoring scoring, trust_reports, scenario_metrics, factsheet_value…
juanjetm Jun 1, 2026
309e5f0
Refactoring and metrics fixed: Privacy and explainability
juanjetm Jun 3, 2026
e91ffb0
Quality_model and fairness refactored, train accuracy and macro f1 sc…
juanjetm Jun 3, 2026
bc7ef3a
Adversarial Training CAPGD V1
juanjetm Jun 4, 2026
3d232b8
Adversarial Training: Frontend updated and tabular finished, target l…
juanjetm Jun 5, 2026
800b593
CAPGD name changed to constrained PGD. Adult Cencus and Breast Cancer…
juanjetm Jun 5, 2026
c774e9d
Adversarial training for tabular data updated: Margin window implemen…
juanjetm Jun 8, 2026
48c1242
Robustness metrics revised and fixed, trustworthiness metrics revised…
juanjetm Jun 10, 2026
8e0a7a1
Citations
juanjetm Jun 11, 2026
51e6bc3
DP error fixed
juanjetm Jun 11, 2026
f70844f
Merge remote-tracking branch 'upstream/main'
juanjetm Jun 12, 2026
ce3f596
Refactor code for improved readability and consistency
enriquetomasmb Jun 12, 2026
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
4 changes: 2 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ Please fill out the following template to help us review your pull request.
<!-- List any related issues, e.g. "Closes #123" -->

## Signed-off-by
Signed-off-by: *Your Name (email)*
Date: *YYYY-MM-DD*
Signed-off-by: *Your Name (email)*
Date: *YYYY-MM-DD*
4 changes: 2 additions & 2 deletions CLA.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ By submitting a pull request, patch or code snippet, you agree that:
your contribution and that Authors may use, sell or license the
software containing your contribution at its sole discretion.

Signed-off-by: *Enrique Tomás Martínez Beltrán (enriquetomas@um.es)*
Date: *2025-06-25*
Signed-off-by: *Enrique Tomás Martínez Beltrán (enriquetomas@um.es)*
Date: *2025-06-25*
4 changes: 2 additions & 2 deletions COMMERCIAL_INFO.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# NEBULA Enterprise License

This repository is published under **GNU AGPL v3.0**.
This repository is published under **GNU AGPL v3.0**.
If you wish to embed NEBULA in closed-source products, offer it as a hosted service, or obtain an SLA, please e-mail **enriquetomas@um.es** and **alberto.huertas@um.es**.

A bespoke commercial agreement (OEM / subscription / SaaS) will be provided on request.
A bespoke commercial agreement (OEM / subscription / SaaS) will be provided on request.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Follow conventional-commit style.
## 2 • Sign the CLA
When you open your first Pull Request, **CLA-assistant** will block the
merge until you tick the box confirming you accept the
[ICLA](CLA.md).
[ICLA](CLA.md).
Add a Developer-Certificate-of-Origin line in every commit:

```
Expand All @@ -23,4 +23,4 @@ The pull request will be reviewed by the maintainers.
The maintainers will provide feedback on the pull request.

## 6 • Merge the Pull Request
The pull request will be merged by the maintainers.
The pull request will be merged by the maintainers.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,33 @@ We would like to thank the following projects for their contributions which have
- [FastAPI](https://github.com/tiangolo/fastapi) for the RESTful API
- [Fedstellar](https://github.com/CyberDataLab/fedstellar) platform and [p2pfl](https://github.com/pguijas/p2pfl/) library
- [Adversarial Robustness Toolbox (ART)](https://github.com/Trusted-AI/adversarial-robustness-toolbox) for the implementation of adversarial attacks
- [Opacus](https://github.com/meta-pytorch/opacus) for differential privacy training support
- [AI Fairness 360 (AIF360)](https://github.com/Trusted-AI/AIF360) for fairness metric definitions
- [HolisticAI](https://github.com/holistic-ai/holisticai) for trustworthiness and fairness metric definitions
- [D3.js](https://github.com/d3/d3-force) for the network visualizations

## Third-party Differential Privacy

NEBULA uses Opacus for differential privacy training:

- Yousefpour, A., Shilov, I., Sablayrolles, A., Testuggine, D., Prasad, K., Malek, M., Nguyen, J., Ghosh, S., Bharadwaj, A., Zhao, J., Cormode, G., & Mironov, I. (2021). Opacus: User-Friendly Differential Privacy Library in PyTorch. arXiv:2109.12298. Licensed under Apache License 2.0: https://github.com/meta-pytorch/opacus/blob/main/LICENSE

## Third-party Trustworthiness Metrics

NEBULA implements some trustworthiness and fairness metrics following definitions documented in external toolkits:

- AI Fairness 360 (AIF360). AI Fairness 360 [Software]. https://github.com/Trusted-AI/AIF360. Licensed under Apache License 2.0: https://github.com/Trusted-AI/AIF360/blob/main/LICENSE

- Holistic AI. HolisticAI [Software]. https://github.com/holistic-ai/holisticai. Licensed under Apache License 2.0: https://github.com/holistic-ai/holisticai/blob/main/LICENSE

## Third-party Tabular Datasets

NEBULA preprocesses these datasets for experiments, including splitting, scaling, encoding, label mapping, filtering, and/or sample limiting depending on the dataset.

- Becker, B. & Kohavi, R. (1996). Adult [Dataset]. UCI Machine Learning Repository. https://doi.org/10.24432/C5XW20. Licensed under CC BY 4.0: https://creativecommons.org/licenses/by/4.0/

- Blackard, J. (1998). Covertype [Dataset]. UCI Machine Learning Repository. https://doi.org/10.24432/C50K5N. Licensed under CC BY 4.0: https://creativecommons.org/licenses/by/4.0/

- Wolberg, W., Mangasarian, O., Street, N., & Street, W. (1993). Breast Cancer Wisconsin (Diagnostic) [Dataset]. UCI Machine Learning Repository. https://doi.org/10.24432/C5DW2B. Licensed under CC BY 4.0: https://creativecommons.org/licenses/by/4.0/

- Stolfo, S., Fan, W., Lee, W., Prodromidis, A., & Chan, P. (1999). KDD Cup 1999 Data [Dataset]. UCI Machine Learning Repository. https://doi.org/10.24432/C51C7N. Licensed under CC BY 4.0: https://creativecommons.org/licenses/by/4.0/
30 changes: 15 additions & 15 deletions app/deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,17 +289,17 @@ def run_script(self, script):
def kill_script_processes(self, pids_file):
"""
Forcefully terminates processes listed in a given PID file, including their child processes.

Args:
pids_file (str): Path to the file containing PIDs, one per line.

Behavior:
- Reads the PIDs from the file.
- For each PID, checks if the process exists.
- If it exists, kills all child processes recursively before killing the main process.
- Handles and logs exceptions such as missing processes or invalid PID entries.
- Logs warnings and errors appropriately.

Typical use case:
Used to clean up running processes related to a scenario or script that has been deleted or stopped.
"""
Expand Down Expand Up @@ -344,7 +344,7 @@ def run_observer():
"""
Starts a watchdog observer to monitor the configuration directory for changes.

This function is typically used to execute additional scripts or trigger events
This function is typically used to execute additional scripts or trigger events
during the execution of a federated learning session by monitoring file system changes.

Main functionalities:
Expand All @@ -357,7 +357,7 @@ def run_observer():
- Trigger specific actions during a federation lifecycle.

Note:
The observer runs in a blocking mode and will keep the process alive
The observer runs in a blocking mode and will keep the process alive
until manually stopped or interrupted.
"""
# Watchdog for running additional scripts in the host machine (i.e. during the execution of a federation)
Expand All @@ -373,7 +373,7 @@ class Deployer:
"""
Handles the configuration and initialization of deployment parameters for the NEBULA system.

This class reads and stores various deployment-related settings such as port assignments,
This class reads and stores various deployment-related settings such as port assignments,
environment paths, logging configuration, and system mode (production, development, or simulation).

Main functionalities:
Expand Down Expand Up @@ -438,7 +438,7 @@ def configure_logger(self):
"""
Configures the logging system for the deployment controller.

This method sets up both console and file logging with a consistent format and appropriate log levels.
This method sets up both console and file logging with a consistent format and appropriate log levels.
It also ensures that Uvicorn loggers are properly configured to avoid duplicate log outputs.

Main functionalities:
Expand All @@ -452,7 +452,7 @@ def configure_logger(self):
- Ensures clean and consistent logging output during deployment.

Note:
This method does not set up file logging directly, but prepares the base configuration
This method does not set up file logging directly, but prepares the base configuration
and Uvicorn logger behavior for further logging use.
"""
log_console_format = "[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s"
Expand All @@ -475,7 +475,7 @@ def ensure_directory_access(self, directory_path: str) -> str:
"""
Ensures that the specified directory exists and is writable.

This method attempts to create the directory if it does not exist and verifies
This method attempts to create the directory if it does not exist and verifies
write access by writing and deleting a temporary metadata file.

Args:
Expand Down Expand Up @@ -521,8 +521,8 @@ def start(self):
"""
Starts the NEBULA deployment process and all associated services.

This method initializes the NEBULA platform by setting up the environment,
checking port availability, starting key services (controller, frontend, WAF),
This method initializes the NEBULA platform by setting up the environment,
checking port availability, starting key services (controller, frontend, WAF),
and launching a filesystem observer to react to configuration changes.

Main functionalities:
Expand All @@ -539,7 +539,7 @@ def start(self):
- Central entry point for managing NEBULA components during deployment.

Note:
The method blocks indefinitely until manually interrupted,
The method blocks indefinitely until manually interrupted,
and ensures graceful shutdown upon receiving SIGINT or SIGTERM.
"""
banner = """
Expand Down Expand Up @@ -616,8 +616,8 @@ def signal_handler(self, sig, frame):
"""
Handles system termination signals to ensure a clean shutdown.

This method is triggered when the application receives SIGTERM or SIGINT signals
(e.g., via Ctrl+C or `kill`). It logs the event, performs cleanup actions, and
This method is triggered when the application receives SIGTERM or SIGINT signals
(e.g., via Ctrl+C or `kill`). It logs the event, performs cleanup actions, and
terminates the process gracefully.

Args:
Expand Down Expand Up @@ -749,7 +749,7 @@ def run_controller(self):
)

network_name = f"{os.environ['USER']}_nebula-net-base"

try:
subprocess.check_call(["nvidia-smi"])
self.gpu_available = True
Expand Down
2 changes: 1 addition & 1 deletion app/windows/install.ps1
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Run make install
make install
make install
10 changes: 5 additions & 5 deletions docs/_prebuilt/commercial-faq.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Commercial FAQ — NEBULA Enterprise

**Q 1. What does the commercial license cover?**
**Q 1. What does the commercial license cover?**
To be determined.

**Q 2. Does the commercial edition include extra features?**
**Q 2. Does the commercial edition include extra features?**
To be determined.

**Q 3. Pricing model?**
**Q 3. Pricing model?**
To be determined.

**Q 4. Can we contribute back fixes?**
Absolutely; your patches remain under AGPL in the community edition, and you can keep proprietary extensions private under the commercial agreement.
**Q 4. Can we contribute back fixes?**
Absolutely; your patches remain under AGPL in the community edition, and you can keep proprietary extensions private under the commercial agreement.
4 changes: 2 additions & 2 deletions nebula/addons/attacks/communications/floodingattack.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ async def wrapper(*args, **kwargs):
)
_, *new_args = args # Exclude self argument
await func(*new_args, **kwargs)
_, *new_args = args
_, *new_args = args
return await func(*new_args)

return wrapper

return decorator
79 changes: 67 additions & 12 deletions nebula/addons/attacks/dataset/datapoison.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,59 @@ def _convert_to_tensor(self, data: torch.Tensor | Image.Image | tuple) -> torch.
else:
return torch.tensor(data)

def _restore_data_format(self, data, original):
if isinstance(data, torch.Tensor):
array_data = data.detach().cpu().numpy()
else:
array_data = np.asarray(data)

original_shape = None
if isinstance(original, torch.Tensor):
original_shape = tuple(original.shape)
elif isinstance(original, Image.Image):
original_shape = np.array(original).shape
elif hasattr(original, "shape"):
original_shape = tuple(original.shape)

if original_shape is not None and array_data.shape != original_shape and array_data.size == np.prod(original_shape):
array_data = array_data.reshape(original_shape)

if isinstance(original, torch.Tensor):
restored = torch.as_tensor(array_data, device=original.device)
if original.dtype.is_floating_point:
original_max = original.detach().max() if original.numel() > 0 else torch.tensor(1.0, device=original.device)
if restored.numel() > 0 and original_max > 1 and restored.min() >= 0 and restored.max() <= 1:
restored = restored * original_max
return restored.to(dtype=original.dtype)

if restored.numel() > 0 and restored.min() >= 0 and restored.max() <= 1:
restored = restored * torch.iinfo(original.dtype).max
return restored.clamp(torch.iinfo(original.dtype).min, torch.iinfo(original.dtype).max).to(dtype=original.dtype)

if isinstance(original, Image.Image):
original_array = np.array(original)
restored = self._restore_array_dtype(array_data, original_array.dtype, original_array)
return Image.fromarray(restored, mode=original.mode)

if isinstance(original, np.ndarray):
return self._restore_array_dtype(array_data, original.dtype, original)

return data

def _restore_array_dtype(self, data: np.ndarray, dtype: np.dtype, original: np.ndarray | None = None) -> np.ndarray:
dtype = np.dtype(dtype)
if np.issubdtype(dtype, np.integer):
if data.size > 0 and data.min() >= 0 and data.max() <= 1:
data = data * np.iinfo(dtype).max
return np.rint(np.clip(data, np.iinfo(dtype).min, np.iinfo(dtype).max)).astype(dtype)

if original is not None and data.size > 0 and original.size > 0:
original_max = np.max(original)
if original_max > 1 and data.min() >= 0 and data.max() <= 1:
data = data * original_max

return data.astype(dtype)

def _handle_single_point(self, tensor: torch.Tensor) -> tuple[torch.Tensor, bool]:
"""
Handle single point tensors by reshaping them.
Expand Down Expand Up @@ -100,7 +153,7 @@ def __init__(self, noise_type: str):
"""
self.noise_type = noise_type.lower()

def apply_noise(self, t: torch.Tensor | Image.Image, poisoned_noise_percent: float) -> torch.Tensor:
def apply_noise(self, t: torch.Tensor | Image.Image, poisoned_noise_percent: float):
"""
Applies noise to a tensor based on the specified noise type and poisoning percentage.

Expand All @@ -109,9 +162,10 @@ def apply_noise(self, t: torch.Tensor | Image.Image, poisoned_noise_percent: flo
poisoned_noise_percent: The percentage of noise to be applied (0-100)

Returns:
The tensor with noise applied
The poisoned data in the same format as the input
"""
t = self._convert_to_tensor(t)
original = t[0] if isinstance(t, tuple) else t
t = self._convert_to_tensor(original)
t, is_single_point = self._handle_single_point(t)

arr = t.detach().cpu().numpy()
Expand All @@ -122,21 +176,21 @@ def apply_noise(self, t: torch.Tensor | Image.Image, poisoned_noise_percent: flo
)

if self.noise_type == "salt":
poisoned = torch.tensor(random_noise(arr, mode=self.noise_type, amount=poisoned_ratio))
poisoned = random_noise(arr, mode=self.noise_type, amount=poisoned_ratio)
elif self.noise_type == "gaussian":
poisoned = torch.tensor(random_noise(arr, mode=self.noise_type, mean=0, var=poisoned_ratio, clip=True))
poisoned = random_noise(arr, mode=self.noise_type, mean=0, var=poisoned_ratio, clip=True)
elif self.noise_type == "s&p":
poisoned = torch.tensor(random_noise(arr, mode=self.noise_type, amount=poisoned_ratio))
poisoned = random_noise(arr, mode=self.noise_type, amount=poisoned_ratio)
elif self.noise_type == "nlp_rawdata":
poisoned = self.poison_to_nlp_rawdata(arr, poisoned_ratio)
else:
logging.info(f"ERROR: noise_type '{self.noise_type}' not supported in data poison attack.")
return t
return original

if is_single_point:
poisoned = poisoned[0]

return poisoned
return self._restore_data_format(poisoned, original)

def poison_to_nlp_rawdata(self, text_data: list, poisoned_ratio: float) -> list:
"""
Expand Down Expand Up @@ -221,18 +275,19 @@ def __init__(self, target_label: int):
"""
self.target_label = target_label

def add_x_to_image(self, img: torch.Tensor | Image.Image) -> torch.Tensor:
def add_x_to_image(self, img: torch.Tensor | Image.Image):
"""
Adds a 10x10 pixel 'X' mark to the top-left corner of an image.

Args:
img: Input image tensor or PIL Image

Returns:
Modified image with X pattern
Modified image in the same format as the input
"""
logging.info(f"[{self.__class__.__name__}] Adding X pattern to image")
img = self._convert_to_tensor(img)
original = img[0] if isinstance(img, tuple) else img
img = self._convert_to_tensor(original)
img, is_single_point = self._handle_single_point(img)

# Handle batch dimension if present
Expand Down Expand Up @@ -267,7 +322,7 @@ def add_x_to_image(self, img: torch.Tensor | Image.Image) -> torch.Tensor:
if is_single_point:
img = img[0]

return img
return self._restore_data_format(img, original)

def poison_data(
self,
Expand Down
2 changes: 1 addition & 1 deletion nebula/addons/attacks/model/gllneuroninversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,4 @@ def model_attack(self, received_weights):
# Inject random noise of the same shape and type
received_weights[target_key] = torch.empty_like(target_weights).uniform_(0, noise_scale)

return received_weights
return received_weights
2 changes: 1 addition & 1 deletion nebula/addons/attacks/model/swappingweights.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,4 @@ def model_attack(self, received_weights):
if self.layer_idx + 2 < len(layer_keys):
received_weights[layer_keys[self.layer_idx + 2]] = received_weights[layer_keys[self.layer_idx + 2]][:, perm]

return received_weights
return received_weights
1 change: 1 addition & 0 deletions nebula/addons/defenses/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Defense add-ons for Nebula."""
Loading
Loading