Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mlcube edit command #598

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Inside this repo you can find all important pieces for running MedPerf. In its c

If you use MedPerf, please cite our main paper: Karargyris, A., Umeton, R., Sheller, M.J. et al. Federated benchmarking of medical artificial intelligence with MedPerf. *Nature Machine Intelligence* **5**, 799–810 (2023). [https://www.nature.com/articles/s42256-023-00652-2](https://www.nature.com/articles/s42256-023-00652-2)

Additonally, here you can see how others used MedPerf already: [https://scholar.google.com/scholar?q="medperf"](https://scholar.google.com/scholar?q="medperf").
Additionally, here you can see how others used MedPerf already: [https://scholar.google.com/scholar?q="medperf"](https://scholar.google.com/scholar?q="medperf").

## Experiments

Expand Down
3 changes: 1 addition & 2 deletions cli/cli_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
################### Start Testing ########################
##########################################################


##########################################################
echo "=========================================="
echo "Printing MedPerf version"
Expand Down Expand Up @@ -195,7 +194,7 @@ echo "Running data submission step"
echo "====================================="
print_eval "medperf dataset submit -p $PREP_UID -d $DIRECTORY/dataset_a -l $DIRECTORY/dataset_a --name='dataset_a' --description='mock dataset a' --location='mock location a' -y"
checkFailed "Data submission step failed"
DSET_A_UID=$(medperf dataset ls | grep dataset_a | tr -s ' ' | cut -d ' ' -f 1)
DSET_A_UID=$(medperf dataset ls | grep dataset_a | tr -s ' ' | awk '{$1=$1;print}' | cut -d ' ' -f 1)
echo "DSET_A_UID=$DSET_A_UID"
##########################################################

Expand Down
2 changes: 1 addition & 1 deletion cli/medperf/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def execute(
please run the command again with the --no-cache option.\n"""
)
else:
ResultSubmission.run(result.generated_uid, approved=approval)
ResultSubmission.run(result.local_id, approved=approval)
config.ui.print("✅ Done!")


Expand Down
16 changes: 9 additions & 7 deletions cli/medperf/commands/benchmark/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@
@app.command("ls")
@clean_except
def list(
local: bool = typer.Option(False, "--local", help="Get local benchmarks"),
unregistered: bool = typer.Option(
False, "--unregistered", help="Get unregistered benchmarks"
),
mine: bool = typer.Option(False, "--mine", help="Get current-user benchmarks"),
):
"""List benchmarks stored locally and remotely from the user"""
"""List benchmarks"""
EntityList.run(
Benchmark,
fields=["UID", "Name", "Description", "State", "Approval Status", "Registered"],
local_only=local,
unregistered=unregistered,
mine_only=mine,
)

Expand Down Expand Up @@ -162,10 +164,10 @@ def view(
"--format",
help="Format to display contents. Available formats: [yaml, json]",
),
local: bool = typer.Option(
unregistered: bool = typer.Option(
False,
"--local",
help="Display local benchmarks if benchmark ID is not provided",
"--unregistered",
help="Display unregistered benchmarks if benchmark ID is not provided",
),
mine: bool = typer.Option(
False,
Expand All @@ -180,4 +182,4 @@ def view(
),
):
"""Displays the information of one or more benchmarks"""
EntityView.run(entity_id, Benchmark, format, local, mine, output)
EntityView.run(entity_id, Benchmark, format, unregistered, mine, output)
2 changes: 1 addition & 1 deletion cli/medperf/commands/benchmark/submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def run_compatibility_test(self):
self.ui.print("Running compatibility test")
self.bmk.write()
data_uid, results = CompatibilityTestExecution.run(
benchmark=self.bmk.generated_uid,
benchmark=self.bmk.local_id,
no_cache=self.no_cache,
skip_data_preparation_step=self.skip_data_preparation_step,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ def run(
@clean_except
def list():
"""List previously executed tests reports."""
EntityList.run(TestReport, fields=["UID", "Data Source", "Model", "Evaluator"])
EntityList.run(
TestReport,
fields=["UID", "Data Source", "Model", "Evaluator"],
unregistered=True,
)


@app.command("view")
Expand All @@ -116,4 +120,4 @@ def view(
),
):
"""Displays the information of one or more test reports"""
EntityView.run(entity_id, TestReport, format, output=output)
EntityView.run(entity_id, TestReport, format, unregistered=True, output=output)
2 changes: 1 addition & 1 deletion cli/medperf/commands/compatibility_test/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ def cached_results(self):
"""
if self.no_cache:
return
uid = self.report.generated_uid
uid = self.report.local_id
try:
report = TestReport.get(uid)
except InvalidArgumentError:
Expand Down
16 changes: 8 additions & 8 deletions cli/medperf/commands/compatibility_test/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,23 +138,23 @@ def create_test_dataset(
# TODO: existing dataset could make problems
# make some changes since this is a test dataset
config.tmp_paths.remove(data_creation.dataset.path)
data_creation.dataset.write()
if skip_data_preparation_step:
data_creation.make_dataset_prepared()
dataset = data_creation.dataset
old_generated_uid = dataset.generated_uid
old_path = dataset.path

# prepare/check dataset
DataPreparation.run(dataset.generated_uid)

# update dataset generated_uid
old_path = dataset.path
generated_uid = get_folders_hash([dataset.data_path, dataset.labels_path])
dataset.generated_uid = generated_uid
dataset.write()
if dataset.input_data_hash != dataset.generated_uid:
new_generated_uid = get_folders_hash([dataset.data_path, dataset.labels_path])
if new_generated_uid != old_generated_uid:
# move to a correct location if it underwent preparation
new_path = old_path.replace(dataset.input_data_hash, generated_uid)
new_path = old_path.replace(old_generated_uid, new_generated_uid)
remove_path(new_path)
os.rename(old_path, new_path)
dataset.generated_uid = new_generated_uid
dataset.write()

return generated_uid
return new_generated_uid
16 changes: 10 additions & 6 deletions cli/medperf/commands/dataset/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@
@app.command("ls")
@clean_except
def list(
local: bool = typer.Option(False, "--local", help="Get local datasets"),
unregistered: bool = typer.Option(
False, "--unregistered", help="Get unregistered datasets"
),
mine: bool = typer.Option(False, "--mine", help="Get current-user datasets"),
mlcube: int = typer.Option(
None, "--mlcube", "-m", help="Get datasets for a given data prep mlcube"
),
):
"""List datasets stored locally and remotely from the user"""
"""List datasets"""
EntityList.run(
Dataset,
fields=["UID", "Name", "Data Preparation Cube UID", "State", "Status", "Owner"],
local_only=local,
unregistered=unregistered,
mine_only=mine,
mlcube=mlcube,
)
Expand Down Expand Up @@ -149,8 +151,10 @@ def view(
"--format",
help="Format to display contents. Available formats: [yaml, json]",
),
local: bool = typer.Option(
False, "--local", help="Display local datasets if dataset ID is not provided"
unregistered: bool = typer.Option(
False,
"--unregistered",
help="Display unregistered datasets if dataset ID is not provided",
),
mine: bool = typer.Option(
False,
Expand All @@ -165,4 +169,4 @@ def view(
),
):
"""Displays the information of one or more datasets"""
EntityView.run(entity_id, Dataset, format, local, mine, output)
EntityView.run(entity_id, Dataset, format, unregistered, mine, output)
14 changes: 7 additions & 7 deletions cli/medperf/commands/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,23 @@ def prepare(self):
logging.debug(f"tmp results output: {self.results_path}")

def __setup_logs_path(self):
model_uid = self.model.generated_uid
eval_uid = self.evaluator.generated_uid
data_hash = self.dataset.generated_uid
model_uid = self.model.local_id
eval_uid = self.evaluator.local_id
data_uid = self.dataset.local_id

logs_path = os.path.join(
config.experiments_logs_folder, str(model_uid), str(data_hash)
config.experiments_logs_folder, str(model_uid), str(data_uid)
)
os.makedirs(logs_path, exist_ok=True)
model_logs_path = os.path.join(logs_path, "model.log")
metrics_logs_path = os.path.join(logs_path, f"metrics_{eval_uid}.log")
return model_logs_path, metrics_logs_path

def __setup_predictions_path(self):
model_uid = self.model.generated_uid
data_hash = self.dataset.generated_uid
model_uid = self.model.local_id
data_uid = self.dataset.local_id
preds_path = os.path.join(
config.predictions_folder, str(model_uid), str(data_hash)
config.predictions_folder, str(model_uid), str(data_uid)
)
if os.path.exists(preds_path):
msg = f"Found existing predictions for model {self.model.id} on dataset "
Expand Down
29 changes: 20 additions & 9 deletions cli/medperf/commands/list.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List, Type
from medperf.entities.interface import Entity
from medperf.exceptions import InvalidArgumentError
from tabulate import tabulate

Expand All @@ -8,29 +10,38 @@
class EntityList:
@staticmethod
def run(
entity_class,
fields,
local_only: bool = False,
entity_class: Type[Entity],
fields: List[str],
unregistered: bool = False,
mine_only: bool = False,
**kwargs,
):
"""Lists all local datasets

Args:
local_only (bool, optional): Display all local results. Defaults to False.
mine_only (bool, optional): Display all current-user results. Defaults to False.
unregistered (bool, optional): Display only local unregistered results. Defaults to False.
mine_only (bool, optional): Display all registered current-user results. Defaults to False.
kwargs (dict): Additional parameters for filtering entity lists.
"""
entity_list = EntityList(entity_class, fields, local_only, mine_only, **kwargs)
entity_list = EntityList(
entity_class, fields, unregistered, mine_only, **kwargs
)
entity_list.prepare()
entity_list.validate()
entity_list.filter()
entity_list.display()

def __init__(self, entity_class, fields, local_only, mine_only, **kwargs):
def __init__(
self,
entity_class: Type[Entity],
fields: List[str],
unregistered: bool,
mine_only: bool,
**kwargs,
):
self.entity_class = entity_class
self.fields = fields
self.local_only = local_only
self.unregistered = unregistered
self.mine_only = mine_only
self.filters = kwargs
self.data = []
Expand All @@ -40,7 +51,7 @@ def prepare(self):
self.filters["owner"] = get_medperf_user_data()["id"]

entities = self.entity_class.all(
local_only=self.local_only, filters=self.filters
unregistered=self.unregistered, filters=self.filters
)
self.data = [entity.display_dict() for entity in entities]

Expand Down
112 changes: 112 additions & 0 deletions cli/medperf/commands/mlcube/edit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import logging
from typing import Union

import medperf.config as config
from medperf.entities.cube import Cube
from medperf.entities.edit_cube import EditCubeData


class EditCube:
@classmethod
def run(cls, cube_uid: Union[str, int], mlcube_partial_info: EditCubeData):
"""Update mlcube in the development mode on the medperf server

Args:
cube_uid: uid of cube to modify
mlcube_partial_info (dict): Dictionary containing the modified fields.
"""
ui = config.ui

logging.debug("Downloading initial MLCube..")
edition = cls(cube_uid, mlcube_partial_info)
logging.debug("Validating MLCube DEVELOPMENT state..")
edition.validate_dev_state()

with ui.interactive():
ui.text = "Validating updated MLCube can be downloaded"
logging.debug("Applying MLCube edit..")
edition.apply_and_get_hashes()
ui.text = "Submitting MLCube edit to MedPerf"
logging.debug("Uploading MLCube..")
edition.upload()
edition.write()

def __init__(self, cube_uid: Union[str, int], edit_info: EditCubeData):
self.ui = config.ui
self.cube = Cube.get(cube_uid)
self.edit_info = edit_info

def validate_dev_state(self):
if self.cube.state != "DEVELOPMENT":
raise ValueError("Only cubes in development state can be edited")

def apply_and_get_hashes(self):
cube = self.cube
new = self.edit_info

if new.name:
cube.name = new.name

if new.git_mlcube_url:
cube.git_mlcube_url = new.git_mlcube_url
# Differs from further ifs: if mlcube.yaml url is provided, reset image also
cube.image_hash = ""

if new.mlcube_hash:
cube.mlcube_hash = new.mlcube_hash
elif new.git_mlcube_url is not None:
cube.mlcube_hash = ""

if new.git_parameters_url:
cube.git_parameters_url = new.git_parameters_url

if new.parameters_hash:
cube.parameters_hash = new.parameters_hash
elif new.git_parameters_url is not None:
cube.parameters_hash = ""

if new.image_tarball_url:
cube.image_tarball_url = new.image_tarball_url
# same as with git_mlcube_url
cube.image_hash = ""

if new.image_tarball_hash:
cube.image_tarball_hash = new.image_tarball_hash
elif new.image_tarball_url is not None:
cube.image_tarball_hash = ""

if new.additional_files_tarball_url:
cube.additional_files_tarball_url = new.additional_files_tarball_url

if new.additional_files_tarball_hash:
cube.additional_files_tarball_hash = new.additional_files_tarball_hash
elif new.additional_files_tarball_url is not None:
cube.additional_files_tarball_hash = ""

self.download()

if new.git_mlcube_url and not new.mlcube_hash:
new.mlcube_hash = cube.mlcube_hash
if new.git_parameters_url and not new.parameters_hash:
new.parameters_hash = cube.parameters_hash
if new.image_tarball_url and not new.image_tarball_hash:
new.image_tarball_hash = cube.image_tarball_hash
if new.additional_files_tarball_url and not new.additional_files_tarball_hash:
new.additional_files_tarball_hash = cube.additional_files_tarball_hash
if new.git_mlcube_url or new.image_tarball_url:
new.image_hash = cube.image_hash

def download(self):
logging.debug("removing from filesystem...")
self.cube.remove_from_filesystem()
logging.debug("download config files..")
self.cube.download_config_files()
logging.debug("download run files..")
self.cube.download_run_files()

def upload(self):
updated_body = Cube.edit(self.cube.id, self.edit_info)
self.cube = Cube(**updated_body)

def write(self):
self.cube.write()
Loading
Loading