opsml.cards.run
1# Copyright (c) Shipt, Inc. 2# This source code is licensed under the MIT license found in the 3# LICENSE file in the root directory of this source tree. 4 5# pylint: disable=invalid-name 6 7 8import tempfile 9import uuid 10from functools import cached_property 11from pathlib import Path 12from typing import Any, Dict, List, Optional, Tuple, Union, cast 13 14import joblib 15import numpy as np 16from numpy.typing import NDArray 17from pydantic import model_validator 18 19from opsml.cards.base import ArtifactCard 20from opsml.helpers.logging import ArtifactLogger 21from opsml.helpers.utils import TypeChecker 22from opsml.settings.config import config 23from opsml.storage import client 24from opsml.types import ( 25 Artifact, 26 ArtifactUris, 27 CardType, 28 CommonKwargs, 29 GraphStyle, 30 Metric, 31 Metrics, 32 Param, 33 Params, 34 RegistryTableNames, 35 RegistryType, 36 RunCardRegistry, 37 RunGraph, 38 SaveName, 39) 40 41logger = ArtifactLogger.get_logger() 42 43_List = List[Union[float, int]] 44_Dict = Dict[str, List[Union[float, int]]] 45_YReturn = Union[_List, _Dict] 46_ParseReturn = Tuple[_YReturn, str] 47 48 49def _dump_graph_artifact(graph: RunGraph, name: str, uri: Path) -> Tuple[Path, Path]: 50 """Helper method for saving graph artifacts to storage 51 52 Args: 53 graph: 54 RunGraph object 55 name: 56 Name of graph 57 uri: 58 Uri to store graph artifact 59 """ 60 with tempfile.TemporaryDirectory() as tempdir: 61 lpath = Path(tempdir) / f"{name}.joblib" 62 rpath = (uri / SaveName.GRAPHS.value) / lpath.name 63 joblib.dump(graph.model_dump(), lpath) 64 65 client.storage_client.put(lpath, rpath) 66 67 return lpath, rpath 68 69 70def _decimate_list(array: List[Union[float, int]]) -> List[Union[float, int]]: 71 """Decimates array to no more than 200,000 points 72 73 Args: 74 array: 75 List of floats or ints 76 77 Returns: 78 Decimated array 79 """ 80 length = len(array) 81 if len(array) > 200_000: 82 step = round(length / 200_000) 83 return array[::step] 84 85 return array 86 87 88def _parse_y_to_list( 89 x_length: int, 90 y: Union[List[Union[float, int]], NDArray[Any], Dict[str, Union[List[Union[float, int]], NDArray[Any]]]], 91) -> _ParseReturn: 92 """Helper method for parsing y to list when logging a graph 93 94 Args: 95 x_length: 96 Length of x 97 y: 98 Y values to parse. Can be a list, dictionary or numpy array 99 100 Returns: 101 List or dictionary of y values 102 103 """ 104 # if y is dictionary 105 if isinstance(y, dict): 106 _y: Dict[str, List[Union[float, int]]] = {} 107 108 # common sense constraint 109 if len(y.keys()) > 50: 110 raise ValueError("Too many keys in dictionary. A maximum of 50 keys for y is allowed.") 111 112 for k, v in y.items(): 113 if isinstance(v, np.ndarray): 114 v = v.flatten().tolist() 115 assert isinstance(v, list), "y must be a list or dictionary" 116 v = _decimate_list(v) 117 118 assert x_length == len(v), "x and y must be the same length" 119 _y[k] = v 120 121 return _y, "multi" 122 123 # if y is ndarray 124 if isinstance(y, np.ndarray): 125 y = y.flatten().tolist() 126 assert isinstance(y, list), "y must be a list or dictionary" 127 128 y = _decimate_list(y) 129 assert x_length == len(y), "x and y must be the same length" 130 131 return y, "single" 132 133 # if y is list 134 assert isinstance(y, list), "y must be a list or dictionary" 135 y = _decimate_list(y) 136 assert x_length == len(y), "x and y must be the same length" 137 return y, "single" 138 139 140class RunCard(ArtifactCard): 141 142 """ 143 Create a RunCard from specified arguments. 144 145 Apart from required args, a RunCard must be associated with one of 146 datacard_uid, modelcard_uids or pipelinecard_uid 147 148 Args: 149 name: 150 Run name 151 repository: 152 Repository that this card is associated with 153 contact: 154 Contact to associate with card 155 info: 156 `CardInfo` object containing additional metadata. If provided, it will override any 157 values provided for `name`, `repository`, `contact`, and `version`. 158 159 Name, repository, and contact are required arguments for all cards. They can be provided 160 directly or through a `CardInfo` object. 161 162 datacard_uids: 163 Optional DataCard uids associated with this run 164 modelcard_uids: 165 Optional List of ModelCard uids to associate with this run 166 pipelinecard_uid: 167 Optional PipelineCard uid to associate with this experiment 168 metrics: 169 Optional dictionary of key (str), value (int, float) metric paris. 170 Metrics can also be added via class methods. 171 parameters: 172 Parameters associated with a RunCard 173 artifact_uris: 174 Optional dictionary of artifact uris associated with artifacts. 175 uid: 176 Unique id (assigned if card has been registered) 177 version: 178 Current version (assigned if card has been registered) 179 180 """ 181 182 datacard_uids: List[str] = [] 183 modelcard_uids: List[str] = [] 184 pipelinecard_uid: Optional[str] = None 185 metrics: Metrics = {} 186 parameters: Params = {} 187 artifact_uris: ArtifactUris = {} 188 tags: Dict[str, Union[str, int]] = {} 189 project: Optional[str] = None 190 191 @model_validator(mode="before") 192 @classmethod 193 def validate_defaults_args(cls, card_args: Dict[str, Any]) -> Dict[str, Any]: 194 # add default 195 contact = card_args.get("contact") 196 197 if contact is None: 198 card_args["contact"] = CommonKwargs.UNDEFINED.value 199 200 repository = card_args.get("repository") 201 202 if repository is None: 203 card_args["repository"] = "opsml" 204 205 return card_args 206 207 def add_tag(self, key: str, value: str) -> None: 208 """ 209 Logs tags to current RunCard 210 211 Args: 212 key: 213 Key for tag 214 value: 215 value for tag 216 """ 217 self.tags = {**{key: value}, **self.tags} 218 219 def add_tags(self, tags: Dict[str, str]) -> None: 220 """ 221 Logs tags to current RunCard 222 223 Args: 224 tags: 225 Dictionary of tags 226 """ 227 self.tags = {**tags, **self.tags} 228 229 def log_graph( 230 self, 231 name: str, 232 x: Union[List[Union[float, int]], NDArray[Any]], 233 y: Union[List[Union[float, int]], NDArray[Any], Dict[str, Union[List[Union[float, int]], NDArray[Any]]]], 234 y_label: str, 235 x_label: str, 236 graph_style: str, 237 ) -> None: 238 """Logs a graph to the RunCard, which will be rendered in the UI as a line graph 239 240 Args: 241 name: 242 Name of graph 243 x: 244 List or numpy array of x values 245 246 x_label: 247 Label for x axis 248 y: 249 Either a list or numpy array of y values or a dictionary of y values where key is the group label and 250 value is a list or numpy array of y values 251 y_label: 252 Label for y axis 253 graph_style: 254 Style of graph. Options are "line" or "scatter" 255 256 example: 257 258 ### single line graph 259 x = np.arange(1, 400, 0.5) 260 y = x * x 261 run.log_graph(name="graph1", x=x, y=y, x_label="x", y_label="y", graph_style="line") 262 263 ### multi line graph 264 x = np.arange(1, 1000, 0.5) 265 y1 = x * x 266 y2 = y1 * 1.1 267 y3 = y2 * 3 268 run.log_graph( 269 name="multiline", 270 x=x, 271 y={"y1": y1, "y2": y2, "y3": y3}, 272 x_label="x", 273 y_label="y", 274 graph_style="line", 275 ) 276 277 """ 278 279 if isinstance(x, np.ndarray): 280 x = x.flatten().tolist() 281 assert isinstance(x, list), "x must be a list or dictionary" 282 283 x = _decimate_list(x) 284 285 parsed_y, graph_type = _parse_y_to_list(len(x), y) 286 287 logger.info(f"Logging graph {name} to RunCard") 288 graph = RunGraph( 289 name=name, 290 x=x, 291 x_label=x_label, 292 y=parsed_y, 293 y_label=y_label, 294 graph_type=graph_type, 295 graph_style=GraphStyle.from_str(graph_style).value, # validate graph style 296 ) 297 298 # save graph to storage so we can view in ui while run is active 299 lpath, rpath = _dump_graph_artifact(graph, name, self.uri) 300 301 self._add_artifact_uri( 302 name=name, 303 local_path=lpath.as_posix(), 304 remote_path=rpath.as_posix(), 305 ) 306 307 def log_parameters(self, parameters: Dict[str, Union[float, int, str]]) -> None: 308 """ 309 Logs parameters to current RunCard 310 311 Args: 312 parameters: 313 Dictionary of parameters 314 """ 315 316 for key, value in parameters.items(): 317 # check key 318 self.log_parameter(key, value) 319 320 def log_parameter(self, key: str, value: Union[int, float, str]) -> None: 321 """ 322 Logs parameter to current RunCard 323 324 Args: 325 key: 326 Param name 327 value: 328 Param value 329 """ 330 331 TypeChecker.check_param_type(param=value) 332 _key = TypeChecker.replace_spaces(key) 333 334 param = Param(name=key, value=value) 335 336 if self.parameters.get(_key) is not None: 337 self.parameters[_key].append(param) 338 339 else: 340 self.parameters[_key] = [param] 341 342 def log_metric( 343 self, 344 key: str, 345 value: Union[int, float], 346 timestamp: Optional[int] = None, 347 step: Optional[int] = None, 348 ) -> None: 349 """ 350 Logs metric to the existing RunCard metric dictionary 351 352 Args: 353 key: 354 Metric name 355 value: 356 Metric value 357 timestamp: 358 Optional timestamp 359 step: 360 Optional step associated with name and value 361 """ 362 363 TypeChecker.check_metric_type(metric=value) 364 _key = TypeChecker.replace_spaces(key) 365 366 metric = Metric(name=_key, value=value, timestamp=timestamp, step=step) 367 368 self._registry.insert_metric([{**metric.model_dump(), **{"run_uid": self.uid}}]) 369 370 if self.metrics.get(_key) is not None: 371 self.metrics[_key].append(metric) 372 else: 373 self.metrics[_key] = [metric] 374 375 def log_metrics(self, metrics: Dict[str, Union[float, int]], step: Optional[int] = None) -> None: 376 """ 377 Log metrics to the existing RunCard metric dictionary 378 379 Args: 380 metrics: 381 Dictionary containing key (str) and value (float or int) pairs 382 to add to the current metric set 383 step: 384 Optional step associated with metrics 385 """ 386 387 for key, value in metrics.items(): 388 self.log_metric(key, value, step) 389 390 def log_artifact_from_file( 391 self, 392 name: str, 393 local_path: Union[str, Path], 394 artifact_path: Optional[Union[str, Path]] = None, 395 ) -> None: 396 """ 397 Log a local file or directory to the opsml server and associate with the current run. 398 399 Args: 400 name: 401 Name to assign to artifact(s) 402 local_path: 403 Local path to file or directory. Can be string or pathlike object 404 artifact_path: 405 Optional path to store artifact in opsml server. If not provided, 'artifacts' will be used 406 """ 407 408 lpath = Path(local_path) 409 rpath = self.uri / (artifact_path or SaveName.ARTIFACTS.value) 410 411 if lpath.is_file(): 412 rpath = rpath / lpath.name 413 414 client.storage_client.put(lpath, rpath) 415 self._add_artifact_uri( 416 name=name, 417 local_path=lpath.as_posix(), 418 remote_path=rpath.as_posix(), 419 ) 420 421 def create_registry_record(self) -> Dict[str, Any]: 422 """Creates a registry record from the current RunCard""" 423 424 exclude_attr = {"parameters", "metrics"} 425 426 return self.model_dump(exclude=exclude_attr) 427 428 def _add_artifact_uri(self, name: str, local_path: str, remote_path: str) -> None: 429 """ 430 Adds an artifact_uri to the runcard 431 432 Args: 433 name: 434 Name to associate with artifact 435 uri: 436 Uri where artifact is stored 437 """ 438 439 self.artifact_uris[name] = Artifact( 440 name=name, 441 local_path=local_path, 442 remote_path=remote_path, 443 ) 444 445 def add_card_uid(self, card_type: str, uid: str) -> None: 446 """ 447 Adds a card uid to the appropriate card uid list for tracking 448 449 Args: 450 card_type: 451 ArtifactCard class name 452 uid: 453 Uid of registered ArtifactCard 454 """ 455 456 if card_type == CardType.DATACARD: 457 self.datacard_uids = [uid, *self.datacard_uids] 458 elif card_type == CardType.MODELCARD: 459 self.modelcard_uids = [uid, *self.modelcard_uids] 460 461 def get_metric(self, name: str) -> Union[List[Metric], Metric]: 462 """ 463 Gets a metric by name 464 465 Args: 466 name: 467 Name of metric 468 469 Returns: 470 List of dictionaries or dictionary containing value 471 472 """ 473 _key = TypeChecker.replace_spaces(name) 474 475 metric = self.metrics.get(_key) 476 477 if metric is None: 478 # try to get metric from registry 479 assert self.uid is not None, "RunCard must be registered to get metric" 480 _metric = self._registry.get_metric(run_uid=self.uid, name=[_key]) 481 482 if _metric is not None: 483 metric = [Metric(**i) for i in _metric] 484 485 else: 486 raise ValueError(f"Metric {metric} was not defined") 487 488 if len(metric) > 1: 489 return metric 490 if len(metric) == 1: 491 return metric[0] 492 return metric 493 494 def load_metrics(self) -> None: 495 """Reloads metrics from registry""" 496 assert self.uid is not None, "RunCard must be registered to load metrics" 497 498 metrics = self._registry.get_metric(run_uid=self.uid) 499 500 if metrics is None: 501 logger.info("No metrics found for RunCard") 502 return None 503 504 # reset metrics 505 self.metrics = {} 506 for metric in metrics: 507 _metric = Metric(**metric) 508 if _metric.name not in self.metrics: 509 self.metrics[_metric.name] = [_metric] 510 else: 511 self.metrics[_metric.name].append(_metric) 512 return None 513 514 def get_parameter(self, name: str) -> Union[List[Param], Param]: 515 """ 516 Gets a parameter by name 517 518 Args: 519 name: 520 Name of parameter 521 522 Returns: 523 List of dictionaries or dictionary containing value 524 525 """ 526 _key = TypeChecker.replace_spaces(name) 527 param = self.parameters.get(_key) 528 if param is not None: 529 if len(param) > 1: 530 return param 531 if len(param) == 1: 532 return param[0] 533 return param 534 535 raise ValueError(f"Param {param} is not defined") 536 537 def load_artifacts(self, name: Optional[str] = None) -> None: 538 """Loads artifacts from artifact_uris""" 539 if bool(self.artifact_uris) is False: 540 logger.info("No artifact uris associated with RunCard") 541 return None 542 543 if name is not None: 544 artifact = self.artifact_uris.get(name) 545 assert artifact is not None, f"Artifact {name} not found" 546 client.storage_client.get( 547 Path(artifact.remote_path), 548 Path(artifact.local_path), 549 ) 550 551 else: 552 for _, artifact in self.artifact_uris.items(): 553 client.storage_client.get( 554 Path(artifact.remote_path), 555 Path(artifact.local_path), 556 ) 557 return None 558 559 @property 560 def uri(self) -> Path: 561 """The base URI to use for the card and it's artifacts.""" 562 563 # when using runcard outside of run context 564 if self.version == CommonKwargs.BASE_VERSION.value: 565 if self.uid is None: 566 self.uid = uuid.uuid4().hex 567 568 end_path = self.uid 569 else: 570 end_path = f"v{self.version}" 571 572 return Path( 573 config.storage_root, 574 RegistryTableNames.from_str(self.card_type).value, 575 str(self.repository), 576 str(self.name), 577 end_path, 578 ) 579 580 @cached_property 581 def _registry(self) -> RunCardRegistry: 582 from opsml.registry.backend import _set_registry 583 584 return cast(RunCardRegistry, _set_registry(RegistryType.RUN)) 585 586 @property 587 def card_type(self) -> str: 588 return CardType.RUNCARD.value
141class RunCard(ArtifactCard): 142 143 """ 144 Create a RunCard from specified arguments. 145 146 Apart from required args, a RunCard must be associated with one of 147 datacard_uid, modelcard_uids or pipelinecard_uid 148 149 Args: 150 name: 151 Run name 152 repository: 153 Repository that this card is associated with 154 contact: 155 Contact to associate with card 156 info: 157 `CardInfo` object containing additional metadata. If provided, it will override any 158 values provided for `name`, `repository`, `contact`, and `version`. 159 160 Name, repository, and contact are required arguments for all cards. They can be provided 161 directly or through a `CardInfo` object. 162 163 datacard_uids: 164 Optional DataCard uids associated with this run 165 modelcard_uids: 166 Optional List of ModelCard uids to associate with this run 167 pipelinecard_uid: 168 Optional PipelineCard uid to associate with this experiment 169 metrics: 170 Optional dictionary of key (str), value (int, float) metric paris. 171 Metrics can also be added via class methods. 172 parameters: 173 Parameters associated with a RunCard 174 artifact_uris: 175 Optional dictionary of artifact uris associated with artifacts. 176 uid: 177 Unique id (assigned if card has been registered) 178 version: 179 Current version (assigned if card has been registered) 180 181 """ 182 183 datacard_uids: List[str] = [] 184 modelcard_uids: List[str] = [] 185 pipelinecard_uid: Optional[str] = None 186 metrics: Metrics = {} 187 parameters: Params = {} 188 artifact_uris: ArtifactUris = {} 189 tags: Dict[str, Union[str, int]] = {} 190 project: Optional[str] = None 191 192 @model_validator(mode="before") 193 @classmethod 194 def validate_defaults_args(cls, card_args: Dict[str, Any]) -> Dict[str, Any]: 195 # add default 196 contact = card_args.get("contact") 197 198 if contact is None: 199 card_args["contact"] = CommonKwargs.UNDEFINED.value 200 201 repository = card_args.get("repository") 202 203 if repository is None: 204 card_args["repository"] = "opsml" 205 206 return card_args 207 208 def add_tag(self, key: str, value: str) -> None: 209 """ 210 Logs tags to current RunCard 211 212 Args: 213 key: 214 Key for tag 215 value: 216 value for tag 217 """ 218 self.tags = {**{key: value}, **self.tags} 219 220 def add_tags(self, tags: Dict[str, str]) -> None: 221 """ 222 Logs tags to current RunCard 223 224 Args: 225 tags: 226 Dictionary of tags 227 """ 228 self.tags = {**tags, **self.tags} 229 230 def log_graph( 231 self, 232 name: str, 233 x: Union[List[Union[float, int]], NDArray[Any]], 234 y: Union[List[Union[float, int]], NDArray[Any], Dict[str, Union[List[Union[float, int]], NDArray[Any]]]], 235 y_label: str, 236 x_label: str, 237 graph_style: str, 238 ) -> None: 239 """Logs a graph to the RunCard, which will be rendered in the UI as a line graph 240 241 Args: 242 name: 243 Name of graph 244 x: 245 List or numpy array of x values 246 247 x_label: 248 Label for x axis 249 y: 250 Either a list or numpy array of y values or a dictionary of y values where key is the group label and 251 value is a list or numpy array of y values 252 y_label: 253 Label for y axis 254 graph_style: 255 Style of graph. Options are "line" or "scatter" 256 257 example: 258 259 ### single line graph 260 x = np.arange(1, 400, 0.5) 261 y = x * x 262 run.log_graph(name="graph1", x=x, y=y, x_label="x", y_label="y", graph_style="line") 263 264 ### multi line graph 265 x = np.arange(1, 1000, 0.5) 266 y1 = x * x 267 y2 = y1 * 1.1 268 y3 = y2 * 3 269 run.log_graph( 270 name="multiline", 271 x=x, 272 y={"y1": y1, "y2": y2, "y3": y3}, 273 x_label="x", 274 y_label="y", 275 graph_style="line", 276 ) 277 278 """ 279 280 if isinstance(x, np.ndarray): 281 x = x.flatten().tolist() 282 assert isinstance(x, list), "x must be a list or dictionary" 283 284 x = _decimate_list(x) 285 286 parsed_y, graph_type = _parse_y_to_list(len(x), y) 287 288 logger.info(f"Logging graph {name} to RunCard") 289 graph = RunGraph( 290 name=name, 291 x=x, 292 x_label=x_label, 293 y=parsed_y, 294 y_label=y_label, 295 graph_type=graph_type, 296 graph_style=GraphStyle.from_str(graph_style).value, # validate graph style 297 ) 298 299 # save graph to storage so we can view in ui while run is active 300 lpath, rpath = _dump_graph_artifact(graph, name, self.uri) 301 302 self._add_artifact_uri( 303 name=name, 304 local_path=lpath.as_posix(), 305 remote_path=rpath.as_posix(), 306 ) 307 308 def log_parameters(self, parameters: Dict[str, Union[float, int, str]]) -> None: 309 """ 310 Logs parameters to current RunCard 311 312 Args: 313 parameters: 314 Dictionary of parameters 315 """ 316 317 for key, value in parameters.items(): 318 # check key 319 self.log_parameter(key, value) 320 321 def log_parameter(self, key: str, value: Union[int, float, str]) -> None: 322 """ 323 Logs parameter to current RunCard 324 325 Args: 326 key: 327 Param name 328 value: 329 Param value 330 """ 331 332 TypeChecker.check_param_type(param=value) 333 _key = TypeChecker.replace_spaces(key) 334 335 param = Param(name=key, value=value) 336 337 if self.parameters.get(_key) is not None: 338 self.parameters[_key].append(param) 339 340 else: 341 self.parameters[_key] = [param] 342 343 def log_metric( 344 self, 345 key: str, 346 value: Union[int, float], 347 timestamp: Optional[int] = None, 348 step: Optional[int] = None, 349 ) -> None: 350 """ 351 Logs metric to the existing RunCard metric dictionary 352 353 Args: 354 key: 355 Metric name 356 value: 357 Metric value 358 timestamp: 359 Optional timestamp 360 step: 361 Optional step associated with name and value 362 """ 363 364 TypeChecker.check_metric_type(metric=value) 365 _key = TypeChecker.replace_spaces(key) 366 367 metric = Metric(name=_key, value=value, timestamp=timestamp, step=step) 368 369 self._registry.insert_metric([{**metric.model_dump(), **{"run_uid": self.uid}}]) 370 371 if self.metrics.get(_key) is not None: 372 self.metrics[_key].append(metric) 373 else: 374 self.metrics[_key] = [metric] 375 376 def log_metrics(self, metrics: Dict[str, Union[float, int]], step: Optional[int] = None) -> None: 377 """ 378 Log metrics to the existing RunCard metric dictionary 379 380 Args: 381 metrics: 382 Dictionary containing key (str) and value (float or int) pairs 383 to add to the current metric set 384 step: 385 Optional step associated with metrics 386 """ 387 388 for key, value in metrics.items(): 389 self.log_metric(key, value, step) 390 391 def log_artifact_from_file( 392 self, 393 name: str, 394 local_path: Union[str, Path], 395 artifact_path: Optional[Union[str, Path]] = None, 396 ) -> None: 397 """ 398 Log a local file or directory to the opsml server and associate with the current run. 399 400 Args: 401 name: 402 Name to assign to artifact(s) 403 local_path: 404 Local path to file or directory. Can be string or pathlike object 405 artifact_path: 406 Optional path to store artifact in opsml server. If not provided, 'artifacts' will be used 407 """ 408 409 lpath = Path(local_path) 410 rpath = self.uri / (artifact_path or SaveName.ARTIFACTS.value) 411 412 if lpath.is_file(): 413 rpath = rpath / lpath.name 414 415 client.storage_client.put(lpath, rpath) 416 self._add_artifact_uri( 417 name=name, 418 local_path=lpath.as_posix(), 419 remote_path=rpath.as_posix(), 420 ) 421 422 def create_registry_record(self) -> Dict[str, Any]: 423 """Creates a registry record from the current RunCard""" 424 425 exclude_attr = {"parameters", "metrics"} 426 427 return self.model_dump(exclude=exclude_attr) 428 429 def _add_artifact_uri(self, name: str, local_path: str, remote_path: str) -> None: 430 """ 431 Adds an artifact_uri to the runcard 432 433 Args: 434 name: 435 Name to associate with artifact 436 uri: 437 Uri where artifact is stored 438 """ 439 440 self.artifact_uris[name] = Artifact( 441 name=name, 442 local_path=local_path, 443 remote_path=remote_path, 444 ) 445 446 def add_card_uid(self, card_type: str, uid: str) -> None: 447 """ 448 Adds a card uid to the appropriate card uid list for tracking 449 450 Args: 451 card_type: 452 ArtifactCard class name 453 uid: 454 Uid of registered ArtifactCard 455 """ 456 457 if card_type == CardType.DATACARD: 458 self.datacard_uids = [uid, *self.datacard_uids] 459 elif card_type == CardType.MODELCARD: 460 self.modelcard_uids = [uid, *self.modelcard_uids] 461 462 def get_metric(self, name: str) -> Union[List[Metric], Metric]: 463 """ 464 Gets a metric by name 465 466 Args: 467 name: 468 Name of metric 469 470 Returns: 471 List of dictionaries or dictionary containing value 472 473 """ 474 _key = TypeChecker.replace_spaces(name) 475 476 metric = self.metrics.get(_key) 477 478 if metric is None: 479 # try to get metric from registry 480 assert self.uid is not None, "RunCard must be registered to get metric" 481 _metric = self._registry.get_metric(run_uid=self.uid, name=[_key]) 482 483 if _metric is not None: 484 metric = [Metric(**i) for i in _metric] 485 486 else: 487 raise ValueError(f"Metric {metric} was not defined") 488 489 if len(metric) > 1: 490 return metric 491 if len(metric) == 1: 492 return metric[0] 493 return metric 494 495 def load_metrics(self) -> None: 496 """Reloads metrics from registry""" 497 assert self.uid is not None, "RunCard must be registered to load metrics" 498 499 metrics = self._registry.get_metric(run_uid=self.uid) 500 501 if metrics is None: 502 logger.info("No metrics found for RunCard") 503 return None 504 505 # reset metrics 506 self.metrics = {} 507 for metric in metrics: 508 _metric = Metric(**metric) 509 if _metric.name not in self.metrics: 510 self.metrics[_metric.name] = [_metric] 511 else: 512 self.metrics[_metric.name].append(_metric) 513 return None 514 515 def get_parameter(self, name: str) -> Union[List[Param], Param]: 516 """ 517 Gets a parameter by name 518 519 Args: 520 name: 521 Name of parameter 522 523 Returns: 524 List of dictionaries or dictionary containing value 525 526 """ 527 _key = TypeChecker.replace_spaces(name) 528 param = self.parameters.get(_key) 529 if param is not None: 530 if len(param) > 1: 531 return param 532 if len(param) == 1: 533 return param[0] 534 return param 535 536 raise ValueError(f"Param {param} is not defined") 537 538 def load_artifacts(self, name: Optional[str] = None) -> None: 539 """Loads artifacts from artifact_uris""" 540 if bool(self.artifact_uris) is False: 541 logger.info("No artifact uris associated with RunCard") 542 return None 543 544 if name is not None: 545 artifact = self.artifact_uris.get(name) 546 assert artifact is not None, f"Artifact {name} not found" 547 client.storage_client.get( 548 Path(artifact.remote_path), 549 Path(artifact.local_path), 550 ) 551 552 else: 553 for _, artifact in self.artifact_uris.items(): 554 client.storage_client.get( 555 Path(artifact.remote_path), 556 Path(artifact.local_path), 557 ) 558 return None 559 560 @property 561 def uri(self) -> Path: 562 """The base URI to use for the card and it's artifacts.""" 563 564 # when using runcard outside of run context 565 if self.version == CommonKwargs.BASE_VERSION.value: 566 if self.uid is None: 567 self.uid = uuid.uuid4().hex 568 569 end_path = self.uid 570 else: 571 end_path = f"v{self.version}" 572 573 return Path( 574 config.storage_root, 575 RegistryTableNames.from_str(self.card_type).value, 576 str(self.repository), 577 str(self.name), 578 end_path, 579 ) 580 581 @cached_property 582 def _registry(self) -> RunCardRegistry: 583 from opsml.registry.backend import _set_registry 584 585 return cast(RunCardRegistry, _set_registry(RegistryType.RUN)) 586 587 @property 588 def card_type(self) -> str: 589 return CardType.RUNCARD.value
Create a RunCard from specified arguments.
Apart from required args, a RunCard must be associated with one of datacard_uid, modelcard_uids or pipelinecard_uid
Arguments:
- name: Run name
- repository: Repository that this card is associated with
- contact: Contact to associate with card
info:
CardInfo
object containing additional metadata. If provided, it will override any values provided forname
,repository
,contact
, andversion
.Name, repository, and contact are required arguments for all cards. They can be provided directly or through a
CardInfo
object.- datacard_uids: Optional DataCard uids associated with this run
- modelcard_uids: Optional List of ModelCard uids to associate with this run
- pipelinecard_uid: Optional PipelineCard uid to associate with this experiment
- metrics: Optional dictionary of key (str), value (int, float) metric paris. Metrics can also be added via class methods.
- parameters: Parameters associated with a RunCard
- artifact_uris: Optional dictionary of artifact uris associated with artifacts.
- uid: Unique id (assigned if card has been registered)
- version: Current version (assigned if card has been registered)
192 @model_validator(mode="before") 193 @classmethod 194 def validate_defaults_args(cls, card_args: Dict[str, Any]) -> Dict[str, Any]: 195 # add default 196 contact = card_args.get("contact") 197 198 if contact is None: 199 card_args["contact"] = CommonKwargs.UNDEFINED.value 200 201 repository = card_args.get("repository") 202 203 if repository is None: 204 card_args["repository"] = "opsml" 205 206 return card_args
208 def add_tag(self, key: str, value: str) -> None: 209 """ 210 Logs tags to current RunCard 211 212 Args: 213 key: 214 Key for tag 215 value: 216 value for tag 217 """ 218 self.tags = {**{key: value}, **self.tags}
Logs tags to current RunCard
Arguments:
- key: Key for tag
- value: value for tag
230 def log_graph( 231 self, 232 name: str, 233 x: Union[List[Union[float, int]], NDArray[Any]], 234 y: Union[List[Union[float, int]], NDArray[Any], Dict[str, Union[List[Union[float, int]], NDArray[Any]]]], 235 y_label: str, 236 x_label: str, 237 graph_style: str, 238 ) -> None: 239 """Logs a graph to the RunCard, which will be rendered in the UI as a line graph 240 241 Args: 242 name: 243 Name of graph 244 x: 245 List or numpy array of x values 246 247 x_label: 248 Label for x axis 249 y: 250 Either a list or numpy array of y values or a dictionary of y values where key is the group label and 251 value is a list or numpy array of y values 252 y_label: 253 Label for y axis 254 graph_style: 255 Style of graph. Options are "line" or "scatter" 256 257 example: 258 259 ### single line graph 260 x = np.arange(1, 400, 0.5) 261 y = x * x 262 run.log_graph(name="graph1", x=x, y=y, x_label="x", y_label="y", graph_style="line") 263 264 ### multi line graph 265 x = np.arange(1, 1000, 0.5) 266 y1 = x * x 267 y2 = y1 * 1.1 268 y3 = y2 * 3 269 run.log_graph( 270 name="multiline", 271 x=x, 272 y={"y1": y1, "y2": y2, "y3": y3}, 273 x_label="x", 274 y_label="y", 275 graph_style="line", 276 ) 277 278 """ 279 280 if isinstance(x, np.ndarray): 281 x = x.flatten().tolist() 282 assert isinstance(x, list), "x must be a list or dictionary" 283 284 x = _decimate_list(x) 285 286 parsed_y, graph_type = _parse_y_to_list(len(x), y) 287 288 logger.info(f"Logging graph {name} to RunCard") 289 graph = RunGraph( 290 name=name, 291 x=x, 292 x_label=x_label, 293 y=parsed_y, 294 y_label=y_label, 295 graph_type=graph_type, 296 graph_style=GraphStyle.from_str(graph_style).value, # validate graph style 297 ) 298 299 # save graph to storage so we can view in ui while run is active 300 lpath, rpath = _dump_graph_artifact(graph, name, self.uri) 301 302 self._add_artifact_uri( 303 name=name, 304 local_path=lpath.as_posix(), 305 remote_path=rpath.as_posix(), 306 )
Logs a graph to the RunCard, which will be rendered in the UI as a line graph
Arguments:
- name: Name of graph
- x: List or numpy array of x values
- x_label: Label for x axis
- y: Either a list or numpy array of y values or a dictionary of y values where key is the group label and value is a list or numpy array of y values
- y_label: Label for y axis
- graph_style: Style of graph. Options are "line" or "scatter"
example:
### single line graph
x = np.arange(1, 400, 0.5)
y = x * x
run.log_graph(name="graph1", x=x, y=y, x_label="x", y_label="y", graph_style="line")
### multi line graph
x = np.arange(1, 1000, 0.5)
y1 = x * x
y2 = y1 * 1.1
y3 = y2 * 3
run.log_graph(
name="multiline",
x=x,
y={"y1": y1, "y2": y2, "y3": y3},
x_label="x",
y_label="y",
graph_style="line",
)
308 def log_parameters(self, parameters: Dict[str, Union[float, int, str]]) -> None: 309 """ 310 Logs parameters to current RunCard 311 312 Args: 313 parameters: 314 Dictionary of parameters 315 """ 316 317 for key, value in parameters.items(): 318 # check key 319 self.log_parameter(key, value)
Logs parameters to current RunCard
Arguments:
- parameters: Dictionary of parameters
321 def log_parameter(self, key: str, value: Union[int, float, str]) -> None: 322 """ 323 Logs parameter to current RunCard 324 325 Args: 326 key: 327 Param name 328 value: 329 Param value 330 """ 331 332 TypeChecker.check_param_type(param=value) 333 _key = TypeChecker.replace_spaces(key) 334 335 param = Param(name=key, value=value) 336 337 if self.parameters.get(_key) is not None: 338 self.parameters[_key].append(param) 339 340 else: 341 self.parameters[_key] = [param]
Logs parameter to current RunCard
Arguments:
- key: Param name
- value: Param value
343 def log_metric( 344 self, 345 key: str, 346 value: Union[int, float], 347 timestamp: Optional[int] = None, 348 step: Optional[int] = None, 349 ) -> None: 350 """ 351 Logs metric to the existing RunCard metric dictionary 352 353 Args: 354 key: 355 Metric name 356 value: 357 Metric value 358 timestamp: 359 Optional timestamp 360 step: 361 Optional step associated with name and value 362 """ 363 364 TypeChecker.check_metric_type(metric=value) 365 _key = TypeChecker.replace_spaces(key) 366 367 metric = Metric(name=_key, value=value, timestamp=timestamp, step=step) 368 369 self._registry.insert_metric([{**metric.model_dump(), **{"run_uid": self.uid}}]) 370 371 if self.metrics.get(_key) is not None: 372 self.metrics[_key].append(metric) 373 else: 374 self.metrics[_key] = [metric]
Logs metric to the existing RunCard metric dictionary
Arguments:
- key: Metric name
- value: Metric value
- timestamp: Optional timestamp
- step: Optional step associated with name and value
376 def log_metrics(self, metrics: Dict[str, Union[float, int]], step: Optional[int] = None) -> None: 377 """ 378 Log metrics to the existing RunCard metric dictionary 379 380 Args: 381 metrics: 382 Dictionary containing key (str) and value (float or int) pairs 383 to add to the current metric set 384 step: 385 Optional step associated with metrics 386 """ 387 388 for key, value in metrics.items(): 389 self.log_metric(key, value, step)
Log metrics to the existing RunCard metric dictionary
Arguments:
- metrics: Dictionary containing key (str) and value (float or int) pairs to add to the current metric set
- step: Optional step associated with metrics
391 def log_artifact_from_file( 392 self, 393 name: str, 394 local_path: Union[str, Path], 395 artifact_path: Optional[Union[str, Path]] = None, 396 ) -> None: 397 """ 398 Log a local file or directory to the opsml server and associate with the current run. 399 400 Args: 401 name: 402 Name to assign to artifact(s) 403 local_path: 404 Local path to file or directory. Can be string or pathlike object 405 artifact_path: 406 Optional path to store artifact in opsml server. If not provided, 'artifacts' will be used 407 """ 408 409 lpath = Path(local_path) 410 rpath = self.uri / (artifact_path or SaveName.ARTIFACTS.value) 411 412 if lpath.is_file(): 413 rpath = rpath / lpath.name 414 415 client.storage_client.put(lpath, rpath) 416 self._add_artifact_uri( 417 name=name, 418 local_path=lpath.as_posix(), 419 remote_path=rpath.as_posix(), 420 )
Log a local file or directory to the opsml server and associate with the current run.
Arguments:
- name: Name to assign to artifact(s)
- local_path: Local path to file or directory. Can be string or pathlike object
- artifact_path: Optional path to store artifact in opsml server. If not provided, 'artifacts' will be used
422 def create_registry_record(self) -> Dict[str, Any]: 423 """Creates a registry record from the current RunCard""" 424 425 exclude_attr = {"parameters", "metrics"} 426 427 return self.model_dump(exclude=exclude_attr)
Creates a registry record from the current RunCard
446 def add_card_uid(self, card_type: str, uid: str) -> None: 447 """ 448 Adds a card uid to the appropriate card uid list for tracking 449 450 Args: 451 card_type: 452 ArtifactCard class name 453 uid: 454 Uid of registered ArtifactCard 455 """ 456 457 if card_type == CardType.DATACARD: 458 self.datacard_uids = [uid, *self.datacard_uids] 459 elif card_type == CardType.MODELCARD: 460 self.modelcard_uids = [uid, *self.modelcard_uids]
Adds a card uid to the appropriate card uid list for tracking
Arguments:
- card_type: ArtifactCard class name
- uid: Uid of registered ArtifactCard
462 def get_metric(self, name: str) -> Union[List[Metric], Metric]: 463 """ 464 Gets a metric by name 465 466 Args: 467 name: 468 Name of metric 469 470 Returns: 471 List of dictionaries or dictionary containing value 472 473 """ 474 _key = TypeChecker.replace_spaces(name) 475 476 metric = self.metrics.get(_key) 477 478 if metric is None: 479 # try to get metric from registry 480 assert self.uid is not None, "RunCard must be registered to get metric" 481 _metric = self._registry.get_metric(run_uid=self.uid, name=[_key]) 482 483 if _metric is not None: 484 metric = [Metric(**i) for i in _metric] 485 486 else: 487 raise ValueError(f"Metric {metric} was not defined") 488 489 if len(metric) > 1: 490 return metric 491 if len(metric) == 1: 492 return metric[0] 493 return metric
Gets a metric by name
Arguments:
- name: Name of metric
Returns:
List of dictionaries or dictionary containing value
495 def load_metrics(self) -> None: 496 """Reloads metrics from registry""" 497 assert self.uid is not None, "RunCard must be registered to load metrics" 498 499 metrics = self._registry.get_metric(run_uid=self.uid) 500 501 if metrics is None: 502 logger.info("No metrics found for RunCard") 503 return None 504 505 # reset metrics 506 self.metrics = {} 507 for metric in metrics: 508 _metric = Metric(**metric) 509 if _metric.name not in self.metrics: 510 self.metrics[_metric.name] = [_metric] 511 else: 512 self.metrics[_metric.name].append(_metric) 513 return None
Reloads metrics from registry
515 def get_parameter(self, name: str) -> Union[List[Param], Param]: 516 """ 517 Gets a parameter by name 518 519 Args: 520 name: 521 Name of parameter 522 523 Returns: 524 List of dictionaries or dictionary containing value 525 526 """ 527 _key = TypeChecker.replace_spaces(name) 528 param = self.parameters.get(_key) 529 if param is not None: 530 if len(param) > 1: 531 return param 532 if len(param) == 1: 533 return param[0] 534 return param 535 536 raise ValueError(f"Param {param} is not defined")
Gets a parameter by name
Arguments:
- name: Name of parameter
Returns:
List of dictionaries or dictionary containing value
538 def load_artifacts(self, name: Optional[str] = None) -> None: 539 """Loads artifacts from artifact_uris""" 540 if bool(self.artifact_uris) is False: 541 logger.info("No artifact uris associated with RunCard") 542 return None 543 544 if name is not None: 545 artifact = self.artifact_uris.get(name) 546 assert artifact is not None, f"Artifact {name} not found" 547 client.storage_client.get( 548 Path(artifact.remote_path), 549 Path(artifact.local_path), 550 ) 551 552 else: 553 for _, artifact in self.artifact_uris.items(): 554 client.storage_client.get( 555 Path(artifact.remote_path), 556 Path(artifact.local_path), 557 ) 558 return None
Loads artifacts from artifact_uris
560 @property 561 def uri(self) -> Path: 562 """The base URI to use for the card and it's artifacts.""" 563 564 # when using runcard outside of run context 565 if self.version == CommonKwargs.BASE_VERSION.value: 566 if self.uid is None: 567 self.uid = uuid.uuid4().hex 568 569 end_path = self.uid 570 else: 571 end_path = f"v{self.version}" 572 573 return Path( 574 config.storage_root, 575 RegistryTableNames.from_str(self.card_type).value, 576 str(self.repository), 577 str(self.name), 578 end_path, 579 )
The base URI to use for the card and it's artifacts.
Inherited Members
- pydantic.main.BaseModel
- BaseModel
- model_extra
- model_fields_set
- model_construct
- model_copy
- model_dump
- model_dump_json
- model_json_schema
- model_parametrized_name
- model_post_init
- model_rebuild
- model_validate
- model_validate_json
- model_validate_strings
- dict
- json
- parse_obj
- parse_raw
- parse_file
- from_orm
- construct
- copy
- schema
- schema_json
- validate
- update_forward_refs