MEDfl 2.0.10__tar.gz → 2.0.10.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/rw/xgboost/strategy.py +303 -15
  2. {medfl-2.0.10 → medfl-2.0.10.dev1}/PKG-INFO +1 -1
  3. {medfl-2.0.10 → medfl-2.0.10.dev1}/medfl.egg-info/PKG-INFO +1 -1
  4. {medfl-2.0.10 → medfl-2.0.10.dev1}/medfl.egg-info/requires.txt +1 -0
  5. {medfl-2.0.10 → medfl-2.0.10.dev1}/setup.py +1 -1
  6. {medfl-2.0.10 → medfl-2.0.10.dev1}/LICENSE +0 -0
  7. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/LearningManager/__init__.py +0 -0
  8. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/LearningManager/client.py +0 -0
  9. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/LearningManager/dynamicModal.py +0 -0
  10. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/LearningManager/federated_dataset.py +0 -0
  11. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/LearningManager/flpipeline.py +0 -0
  12. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/LearningManager/model.py +0 -0
  13. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/LearningManager/params.yaml +0 -0
  14. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/LearningManager/params_optimiser.py +0 -0
  15. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/LearningManager/plot.py +0 -0
  16. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/LearningManager/server.py +0 -0
  17. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/LearningManager/strategy.py +0 -0
  18. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/LearningManager/utils.py +0 -0
  19. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/NetManager/__init__.py +0 -0
  20. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/NetManager/database_connector.py +0 -0
  21. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/NetManager/dataset.py +0 -0
  22. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/NetManager/flsetup.py +0 -0
  23. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/NetManager/net_helper.py +0 -0
  24. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/NetManager/net_manager_queries.py +0 -0
  25. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/NetManager/network.py +0 -0
  26. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/NetManager/node.py +0 -0
  27. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/__init__.py +0 -0
  28. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/rw/__init__.py +0 -0
  29. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/rw/client.py +0 -0
  30. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/rw/model.py +0 -0
  31. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/rw/server.py +0 -0
  32. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/rw/strategy.py +0 -0
  33. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/rw/xgboost/__init__.py +0 -0
  34. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/rw/xgboost/client.py +0 -0
  35. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/rw/xgboost/config.py +0 -0
  36. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/rw/xgboost/data.py +0 -0
  37. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/rw/xgboost/metrics.py +0 -0
  38. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/rw/xgboost/serialization.py +0 -0
  39. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/scripts/__init__.py +0 -0
  40. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/scripts/base.py +0 -0
  41. {medfl-2.0.10 → medfl-2.0.10.dev1}/MEDfl/scripts/create_db.py +0 -0
  42. {medfl-2.0.10 → medfl-2.0.10.dev1}/README.md +0 -0
  43. {medfl-2.0.10 → medfl-2.0.10.dev1}/alembic/__init__.py +0 -0
  44. {medfl-2.0.10 → medfl-2.0.10.dev1}/alembic/env.py +0 -0
  45. {medfl-2.0.10 → medfl-2.0.10.dev1}/medfl.egg-info/SOURCES.txt +0 -0
  46. {medfl-2.0.10 → medfl-2.0.10.dev1}/medfl.egg-info/dependency_links.txt +0 -0
  47. {medfl-2.0.10 → medfl-2.0.10.dev1}/medfl.egg-info/top_level.txt +0 -0
  48. {medfl-2.0.10 → medfl-2.0.10.dev1}/setup.cfg +0 -0
  49. {medfl-2.0.10 → medfl-2.0.10.dev1}/tests/test_dataset.py +0 -0
  50. {medfl-2.0.10 → medfl-2.0.10.dev1}/tests/test_federated_dataset.py +0 -0
  51. {medfl-2.0.10 → medfl-2.0.10.dev1}/tests/test_flpipeline.py +0 -0
  52. {medfl-2.0.10 → medfl-2.0.10.dev1}/tests/test_flsetup.py +0 -0
  53. {medfl-2.0.10 → medfl-2.0.10.dev1}/tests/test_network.py +0 -0
  54. {medfl-2.0.10 → medfl-2.0.10.dev1}/tests/test_node.py +0 -0
@@ -1,6 +1,8 @@
1
1
  import json
2
2
  import os
3
3
  from typing import Dict, Any, Optional
4
+ import tempfile
5
+ import xgboost as xgb
4
6
 
5
7
  import flwr as fl
6
8
  from flwr.common import (
@@ -13,7 +15,7 @@ from flwr.common import GetPropertiesIns
13
15
  from MEDfl.rw.xgboost.config import XGBoostConfig
14
16
  from MEDfl.rw.xgboost.metrics import aggregate_weighted_metrics
15
17
  from MEDfl.rw.xgboost.serialization import (
16
- parameters_to_booster,
18
+ parameters_to_booster,booster_to_parameters,
17
19
  save_booster,
18
20
  XGBOOST_TENSOR_TYPE,
19
21
  )
@@ -108,6 +110,8 @@ class XGBoostStrategy:
108
110
  self.total_rounds = total_rounds
109
111
 
110
112
  self.strategy_object = None
113
+ self.current_booster: Optional[xgb.Booster] = None
114
+ self.current_num_trees: int = 0
111
115
 
112
116
  def create_strategy(self) -> None:
113
117
  """
@@ -278,19 +282,14 @@ class XGBoostStrategy:
278
282
  f"Client:{client.cid}: {fit_res.metrics}"
279
283
  )
280
284
 
285
+ if failures:
286
+ print(f"[Server/XGBoost] ⚠️ Round {server_round} failures: {failures}")
287
+
281
288
  if not results:
282
289
  print("[Server/XGBoost] No fit results received.")
283
290
  return None, {}
284
291
 
285
- # Select booster from largest client
286
- best_client, best_fit_res = max(
287
- results,
288
- key=lambda item: item[1].num_examples,
289
- )
290
-
291
- agg_parameters = best_fit_res.parameters
292
-
293
- # Manual weighted metric aggregation
292
+ # Aggregate metrics manually, same as before.
294
293
  metric_results = []
295
294
 
296
295
  for _, fit_res in results:
@@ -303,18 +302,95 @@ class XGBoostStrategy:
303
302
 
304
303
  metrics = aggregate_weighted_metrics(metric_results)
305
304
 
305
+ previous_num_trees = int(self.current_num_trees)
306
+ new_local_boosters = []
307
+
306
308
  print(
307
- f"[Server/XGBoost] ✅ Round {server_round} - "
308
- f"Selected booster from client {best_client.cid} "
309
- f"with {best_fit_res.num_examples} examples."
309
+ f"[Server/XGBoost] Previous global tree count before round "
310
+ f"{server_round}: {previous_num_trees}"
310
311
  )
311
312
 
313
+ for client, fit_res in results:
314
+ client_booster = parameters_to_booster(fit_res.parameters)
315
+
316
+ if client_booster is None:
317
+ print(
318
+ f"[Server/XGBoost] Client {client.cid} returned no booster. Skipping."
319
+ )
320
+ continue
321
+
322
+ client_total_trees = self._num_boosted_rounds(client_booster)
323
+
324
+ print(
325
+ f"[Server/XGBoost] Client {client.cid} returned booster with "
326
+ f"{client_total_trees} trees."
327
+ )
328
+
329
+ # In round 1, previous_num_trees is 0, so this keeps all local trees.
330
+ # In later rounds, this keeps only the new trees added by the client.
331
+ new_trees_booster = self._slice_new_trees(
332
+ client_booster=client_booster,
333
+ previous_num_trees=previous_num_trees,
334
+ )
335
+
336
+ if new_trees_booster is None:
337
+ continue
338
+
339
+ new_tree_count = self._num_boosted_rounds(new_trees_booster)
340
+
341
+ print(
342
+ f"[Server/XGBoost] Client {client.cid} contributed "
343
+ f"{new_tree_count} new trees."
344
+ )
345
+
346
+ new_local_boosters.append(new_trees_booster)
347
+
348
+ if not new_local_boosters:
349
+ print(
350
+ "[Server/XGBoost] No new local trees received. "
351
+ "Keeping previous global booster."
352
+ )
353
+
354
+ if self.current_booster is None:
355
+ return None, metrics
356
+
357
+ agg_parameters = booster_to_parameters(self.current_booster)
358
+ return agg_parameters, metrics
359
+
360
+ boosters_to_combine = []
361
+
362
+ if self.current_booster is not None:
363
+ boosters_to_combine.append(self.current_booster)
364
+
365
+ boosters_to_combine.extend(new_local_boosters)
366
+
367
+ combined_booster = self._combine_boosters_by_json(boosters_to_combine)
368
+
369
+ if combined_booster is None:
370
+ print("[Server/XGBoost] Failed to combine boosters.")
371
+ return None, metrics
372
+
373
+ self.current_booster = combined_booster
374
+ self.current_num_trees = self._num_boosted_rounds(combined_booster)
375
+
376
+ print(
377
+ f"[Server/XGBoost] ✅ Round {server_round} - Flower-style bagging aggregation done."
378
+ )
379
+ print(
380
+ f"[Server/XGBoost] Global booster now has {self.current_num_trees} trees."
381
+ )
312
382
  print(f"[Server/XGBoost] Aggregated metrics: {metrics}\n")
313
383
 
384
+ # Optional: print importance directly, no saving required.
385
+ if server_round == self.total_rounds:
386
+ self._print_feature_importance(combined_booster, server_round)
387
+
388
+ agg_parameters = booster_to_parameters(combined_booster)
389
+
390
+ # Optional: keep your existing save behavior if savingPath is provided.
314
391
  self._save_if_needed(server_round, agg_parameters)
315
392
 
316
393
  return agg_parameters, metrics
317
-
318
394
  strategy.aggregate_fit = wrapped_aggregate_fit
319
395
 
320
396
 
@@ -377,4 +453,216 @@ class XGBoostStrategy:
377
453
 
378
454
  save_booster(booster, filepath)
379
455
 
380
- print(f"[Server/XGBoost] 💾 Saved booster to: {filepath}")
456
+ print(f"[Server/XGBoost] 💾 Saved booster to: {filepath}")
457
+ def _num_boosted_rounds(self, booster: xgb.Booster) -> int:
458
+ """
459
+ Return the number of boosted rounds/trees in the booster.
460
+ """
461
+ try:
462
+ return int(booster.num_boosted_rounds())
463
+ except Exception:
464
+ try:
465
+ return len(booster.get_dump())
466
+ except Exception:
467
+ return 0
468
+
469
+
470
+ def _slice_new_trees(
471
+ self,
472
+ client_booster: xgb.Booster,
473
+ previous_num_trees: int,
474
+ ) -> Optional[xgb.Booster]:
475
+ """
476
+ Extract only the trees added by the client in this round.
477
+
478
+ Client booster = previous global booster + new local trees.
479
+ We keep only new local trees.
480
+ """
481
+
482
+ total_trees = self._num_boosted_rounds(client_booster)
483
+
484
+ if total_trees <= previous_num_trees:
485
+ print(
486
+ "[Server/XGBoost] Client booster has no new trees. "
487
+ f"total_trees={total_trees}, previous_num_trees={previous_num_trees}"
488
+ )
489
+ return None
490
+
491
+ try:
492
+ return client_booster[previous_num_trees:total_trees]
493
+ except Exception as exc:
494
+ print(f"[Server/XGBoost] Failed to slice new trees: {exc}")
495
+ return None
496
+
497
+
498
+ def _booster_to_raw_bytes(self, booster: xgb.Booster) -> bytes:
499
+ """
500
+ Serialize booster to raw UBJ bytes.
501
+ """
502
+ with tempfile.NamedTemporaryFile(suffix=".ubj", delete=False) as tmp:
503
+ tmp_path = tmp.name
504
+
505
+ try:
506
+ booster.save_model(tmp_path)
507
+ with open(tmp_path, "rb") as f:
508
+ return f.read()
509
+ finally:
510
+ try:
511
+ os.remove(tmp_path)
512
+ except Exception:
513
+ pass
514
+
515
+
516
+ def _booster_from_raw_bytes(self, raw: bytes) -> Optional[xgb.Booster]:
517
+ """
518
+ Deserialize booster from raw UBJ bytes.
519
+ """
520
+ if not raw:
521
+ return None
522
+
523
+ with tempfile.NamedTemporaryFile(suffix=".ubj", delete=False) as tmp:
524
+ tmp_path = tmp.name
525
+ tmp.write(raw)
526
+
527
+ try:
528
+ booster = xgb.Booster()
529
+ booster.load_model(tmp_path)
530
+ return booster
531
+ except Exception as exc:
532
+ print(f"[Server/XGBoost] Failed to load booster from bytes: {exc}")
533
+ return None
534
+ finally:
535
+ try:
536
+ os.remove(tmp_path)
537
+ except Exception:
538
+ pass
539
+
540
+ def _combine_boosters_by_json(
541
+ self,
542
+ boosters: list[xgb.Booster],
543
+ ) -> Optional[xgb.Booster]:
544
+ """
545
+ Combine multiple XGBoost boosters by concatenating their trees.
546
+
547
+ Fixes:
548
+ - updates trees
549
+ - updates tree_info
550
+ - updates gbtree_model_param["num_trees"]
551
+ - updates iteration_indptr
552
+
553
+ This is the missing part that caused:
554
+ Check failed: model.iteration_indptr.back() == model.param.num_trees
555
+ """
556
+
557
+ boosters = [b for b in boosters if b is not None]
558
+
559
+ if not boosters:
560
+ return None
561
+
562
+ if len(boosters) == 1:
563
+ return boosters[0]
564
+
565
+ model_jsons = []
566
+
567
+ for booster in boosters:
568
+ raw_json = booster.save_raw(raw_format="json")
569
+ if isinstance(raw_json, bytes):
570
+ raw_json = raw_json.decode("utf-8")
571
+ model_jsons.append(json.loads(raw_json))
572
+
573
+ base = model_jsons[0]
574
+
575
+ base_model = base["learner"]["gradient_booster"]["model"]
576
+ base_trees = base_model["trees"]
577
+ base_tree_info = base_model["tree_info"]
578
+
579
+ # Start from the base model iteration structure
580
+ base_iteration_indptr = base_model.get("iteration_indptr", None)
581
+
582
+ if base_iteration_indptr is None:
583
+ # Safe fallback for binary/regression: one tree per iteration
584
+ combined_iteration_indptr = list(range(0, len(base_trees) + 1))
585
+ else:
586
+ combined_iteration_indptr = list(base_iteration_indptr)
587
+
588
+ next_tree_id = len(base_trees)
589
+
590
+ for extra in model_jsons[1:]:
591
+ extra_model = extra["learner"]["gradient_booster"]["model"]
592
+
593
+ extra_trees = extra_model["trees"]
594
+ extra_tree_info = extra_model["tree_info"]
595
+
596
+ # Append trees and reassign IDs
597
+ for tree in extra_trees:
598
+ tree["id"] = next_tree_id
599
+ base_trees.append(tree)
600
+ next_tree_id += 1
601
+
602
+ # Append tree_info
603
+ base_tree_info.extend(extra_tree_info)
604
+
605
+ # Append iteration_indptr correctly
606
+ extra_iteration_indptr = extra_model.get("iteration_indptr", None)
607
+
608
+ if extra_iteration_indptr is None:
609
+ # Fallback: one tree per boosting iteration
610
+ extra_iteration_indptr = list(range(0, len(extra_trees) + 1))
611
+
612
+ offset = combined_iteration_indptr[-1]
613
+
614
+ # Skip the first 0 from the extra booster
615
+ for ptr in extra_iteration_indptr[1:]:
616
+ combined_iteration_indptr.append(offset + ptr)
617
+
618
+ # Write back merged model fields
619
+ base_model["trees"] = base_trees
620
+ base_model["tree_info"] = base_tree_info
621
+ base_model["iteration_indptr"] = combined_iteration_indptr
622
+
623
+ # Update num_trees
624
+ if "gbtree_model_param" in base_model:
625
+ base_model["gbtree_model_param"]["num_trees"] = str(len(base_trees))
626
+
627
+ # Save/load to validate the merged model
628
+ with tempfile.NamedTemporaryFile(
629
+ mode="w",
630
+ suffix=".json",
631
+ delete=False,
632
+ encoding="utf-8",
633
+ ) as tmp:
634
+ tmp_path = tmp.name
635
+ json.dump(base, tmp)
636
+
637
+ try:
638
+ combined = xgb.Booster()
639
+ combined.load_model(tmp_path)
640
+ return combined
641
+ finally:
642
+ try:
643
+ os.remove(tmp_path)
644
+ except Exception:
645
+ pass
646
+ def _print_feature_importance(
647
+ self,
648
+ booster: xgb.Booster,
649
+ server_round: int,
650
+ ) -> None:
651
+ """
652
+ Print feature importance for the current global booster.
653
+ """
654
+ if booster is None:
655
+ return
656
+
657
+ importance_gain = booster.get_score(importance_type="gain")
658
+ importance_weight = booster.get_score(importance_type="weight")
659
+ importance_cover = booster.get_score(importance_type="cover")
660
+
661
+ print(f"\n[Server/XGBoost] 📊 Round {server_round} Global Feature Importance - Gain:")
662
+ print(importance_gain)
663
+
664
+ print(f"[Server/XGBoost] 📊 Round {server_round} Global Feature Importance - Weight:")
665
+ print(importance_weight)
666
+
667
+ print(f"[Server/XGBoost] 📊 Round {server_round} Global Feature Importance - Cover:")
668
+ print(importance_cover)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: medfl
3
- Version: 2.0.10
3
+ Version: 2.0.10.dev1
4
4
  Summary: Python Open-source package for simulating federated learning and differential privacy
5
5
  Home-page: https://github.com/MEDomics-UdeS/MEDfl
6
6
  Author: MEDomics consortium
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: medfl
3
- Version: 2.0.10
3
+ Version: 2.0.10.dev1
4
4
  Summary: Python Open-source package for simulating federated learning and differential privacy
5
5
  Home-page: https://github.com/MEDomics-UdeS/MEDfl
6
6
  Author: MEDomics consortium
@@ -14,3 +14,4 @@ PyYAML<7.0,>=6.0
14
14
  Sphinx<6.0,>=5.3
15
15
  sphinx-jsonschema==1.19.1
16
16
  sphinx-rtd-dark-mode==1.2.4
17
+ xgboost
@@ -14,7 +14,7 @@ with open('requirements.txt') as f:
14
14
 
15
15
  setup(
16
16
  name="medfl",
17
- version="2.0.10",
17
+ version="2.0.10.dev1",
18
18
  author="MEDomics consortium",
19
19
  author_email="medomics.info@gmail.com",
20
20
  description="Python Open-source package for simulating federated learning and differential privacy",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes