diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index 09031eef..1aed5b3d 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -69,6 +69,7 @@ from .dependency import Dependable from .issue import IssueType from .license import License, LicenseRepository, _LicenseRepositorySerializationHelper +from .model_card import ModelCard from .release_note import ReleaseNotes @@ -1000,6 +1001,7 @@ def __init__( external_references: Optional[Iterable[ExternalReference]] = None, properties: Optional[Iterable[Property]] = None, release_notes: Optional[ReleaseNotes] = None, + model_card: Optional[ModelCard] = None, cpe: Optional[str] = None, swid: Optional[Swid] = None, pedigree: Optional[Pedigree] = None, @@ -1040,6 +1042,7 @@ def __init__( self.components = components or [] self.evidence = evidence self.release_notes = release_notes + self.model_card = model_card self.crypto_properties = crypto_properties self.tags = tags or [] # spec-deprecated properties below @@ -1597,6 +1600,26 @@ def release_notes(self) -> Optional[ReleaseNotes]: def release_notes(self, release_notes: Optional[ReleaseNotes]) -> None: self._release_notes = release_notes + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(26) + @serializable.json_name('modelCard') + @serializable.xml_name('modelCard') + def model_card(self) -> Optional[ModelCard]: + """ + Specifies the model card for components of type `machine-learning-model`. + + Returns: + `ModelCard` or `None` + """ + return self._model_card + + @model_card.setter + def model_card(self, model_card: Optional[ModelCard]) -> None: + self._model_card = model_card + # @property # ... # @serializable.view(SchemaVersion1Dot5) @@ -1689,7 +1712,7 @@ def __comparable_tuple(self) -> _ComparableTuple: _ComparableTuple(self.external_references), _ComparableTuple(self.properties), _ComparableTuple(self.components), self.evidence, self.release_notes, self.modified, _ComparableTuple(self.authors), _ComparableTuple(self.omnibor_ids), self.manufacturer, - self.crypto_properties, _ComparableTuple(self.tags), + self.crypto_properties, _ComparableTuple(self.tags), self.model_card, )) def __eq__(self, other: object) -> bool: diff --git a/cyclonedx/model/model_card.py b/cyclonedx/model/model_card.py new file mode 100644 index 00000000..536c7207 --- /dev/null +++ b/cyclonedx/model/model_card.py @@ -0,0 +1,1647 @@ +# This file is part of CycloneDX Python Library +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +""" +This set of classes represents the model card types in the CycloneDX standard. + +.. note:: + Introduced in CycloneDX v1.5. Environmental considerations were added in v1.6. + +.. note:: + See the CycloneDX Schema for model cards:\n + - XML: https://cyclonedx.org/docs/1.7/xml/#type_modelCardType\n + - JSON: https://cyclonedx.org/docs/1.7/json/#components_items_modelCard +""" + +from collections.abc import Iterable +from enum import Enum +from typing import Any, Optional, Union + +import py_serializable as serializable +from sortedcontainers import SortedSet + +from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str +from .._internal.compare import ComparableTuple as _ComparableTuple +from ..schema.schema import SchemaVersion1Dot5, SchemaVersion1Dot6, SchemaVersion1Dot7 +from . import AttachedText, ExternalReference, Property +from .bom_ref import BomRef +from .contact import OrganizationalEntity + + +@serializable.serializable_enum +class Co2MeasureUnit(str, Enum): + """Unit of CO2. Currently only tCO2eq is defined by CycloneDX 1.6+.""" + TCO2EQ = 'tCO2eq' + + +@serializable.serializable_enum +class EnergyMeasureUnit(str, Enum): + """Unit of energy. Currently only kWh is defined by CycloneDX 1.6+.""" + KWH = 'kWh' + + +@serializable.serializable_enum +class MachineLearningApproach(str, Enum): + """Enumeration for `machineLearningApproachType`. + + Values are stable across 1.5–1.7. + """ + SUPERVISED = 'supervised' + UNSUPERVISED = 'unsupervised' + REINFORCEMENT_LEARNING = 'reinforcement-learning' + SEMI_SUPERVISED = 'semi-supervised' + SELF_SUPERVISED = 'self-supervised' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class Approach: + """Container for the `approach` element within `modelParameters`.""" + + def __init__(self, *, type: Optional[MachineLearningApproach] = None) -> None: + self.type = type + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + def type(self) -> Optional[MachineLearningApproach]: + return self._type + + @type.setter + def type(self, type: Optional[MachineLearningApproach]) -> None: + self._type = type + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((self.type,)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Approach): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Approach): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, Approach): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, Approach): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class InputOutputMLParameters: + """Definition for items under `modelParameters.inputs[]` and `outputs[]`.""" + + def __init__(self, *, format: str) -> None: + self.format = format + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def format(self) -> str: + return self._format + + @format.setter + def format(self, format: str) -> None: + self._format = format + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((self.format,)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, InputOutputMLParameters): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, InputOutputMLParameters): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, InputOutputMLParameters): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, InputOutputMLParameters): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class ModelParameters: + """`modelParameters` block within `modelCard`.""" + + def __init__( + self, *, + approach: Optional[Approach] = None, + task: Optional[str] = None, + architecture_family: Optional[str] = None, + model_architecture: Optional[str] = None, + datasets: Optional[Iterable[Any]] = None, # Unsupported placeholder until #913 lands. + inputs: Optional[Iterable[InputOutputMLParameters]] = None, + outputs: Optional[Iterable[InputOutputMLParameters]] = None, + ) -> None: + self.approach = approach + self.task = task + self.architecture_family = architecture_family + self.model_architecture = model_architecture + # datasets: The CycloneDX spec allows inline componentData or ref entries. + # This library has not yet implemented component.data (#913). To avoid emitting + # invalid or partial structures, any attempt to populate datasets is rejected. + if datasets is not None: + datasets_list = list(datasets) + if len(datasets_list) > 0: + raise NotImplementedError( + 'modelParameters.datasets is not yet supported. Tracked by issue #913.' + ) + self._datasets: 'SortedSet[Any]' = SortedSet() # always empty until implemented + self.inputs = inputs or [] + self.outputs = outputs or [] + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + def approach(self) -> Optional[Approach]: + return self._approach + + @approach.setter + def approach(self, approach: Optional[Approach]) -> None: + self._approach = approach + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + @serializable.json_name('task') + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + @serializable.xml_name('task') + def task(self) -> Optional[str]: + return self._task + + @task.setter + def task(self, task: Optional[str]) -> None: + self._task = task + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(3) + @serializable.json_name('architectureFamily') + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + @serializable.xml_name('architectureFamily') + def architecture_family(self) -> Optional[str]: + return self._architecture_family + + @architecture_family.setter + def architecture_family(self, architecture_family: Optional[str]) -> None: + self._architecture_family = architecture_family + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(4) + @serializable.json_name('modelArchitecture') + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + @serializable.xml_name('modelArchitecture') + def model_architecture(self) -> Optional[str]: + return self._model_architecture + + @model_architecture.setter + def model_architecture(self, model_architecture: Optional[str]) -> None: + self._model_architecture = model_architecture + + # datasets intentionally omitted from serialization until #913 implemented. + # A future implementation will add a concrete union type and proper annotations. + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(6) + @serializable.json_name('inputs') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'input') + @serializable.xml_name('inputs') + def inputs(self) -> 'SortedSet[InputOutputMLParameters]': + return self._inputs + + @inputs.setter + def inputs(self, inputs: Iterable[InputOutputMLParameters]) -> None: + self._inputs = SortedSet(inputs) + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(7) + @serializable.json_name('outputs') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'output') + @serializable.xml_name('outputs') + def outputs(self) -> 'SortedSet[InputOutputMLParameters]': + return self._outputs + + @outputs.setter + def outputs(self, outputs: Iterable[InputOutputMLParameters]) -> None: + self._outputs = SortedSet(outputs) + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.approach, + self.task, + self.architecture_family, + self.model_architecture, + _ComparableTuple(self._datasets), + _ComparableTuple(self.inputs), + _ComparableTuple(self.outputs), + )) + + def __eq__(self, other: object) -> bool: + if isinstance(other, ModelParameters): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, ModelParameters): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, ModelParameters): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, ModelParameters): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class ConfidenceInterval: + """Confidence interval with lower/upper bounds.""" + + def __init__(self, *, lower_bound: Optional[str] = None, upper_bound: Optional[str] = None) -> None: + self.lower_bound = lower_bound + self.upper_bound = upper_bound + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + @serializable.json_name('lowerBound') + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + @serializable.xml_name('lowerBound') + def lower_bound(self) -> Optional[str]: + return self._lower_bound + + @lower_bound.setter + def lower_bound(self, lower_bound: Optional[str]) -> None: + self._lower_bound = lower_bound + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + @serializable.json_name('upperBound') + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + @serializable.xml_name('upperBound') + def upper_bound(self) -> Optional[str]: + return self._upper_bound + + @upper_bound.setter + def upper_bound(self, upper_bound: Optional[str]) -> None: + self._upper_bound = upper_bound + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((self.lower_bound, self.upper_bound)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, ConfidenceInterval): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, ConfidenceInterval): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, ConfidenceInterval): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, ConfidenceInterval): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class PerformanceMetric: + """A single performance metric entry.""" + + def __init__( + self, *, + type: Optional[str] = None, + value: Optional[str] = None, + slice_: Optional[str] = None, + confidence_interval: Optional[ConfidenceInterval] = None, + ) -> None: + self.type = type + self.value = value + self.slice_ = slice_ + self.confidence_interval = confidence_interval + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def type(self) -> Optional[str]: + return self._type + + @type.setter + def type(self, type: Optional[str]) -> None: + self._type = type + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def value(self) -> Optional[str]: + return self._value + + @value.setter + def value(self, value: Optional[str]) -> None: + self._value = value + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(3) + @serializable.json_name('slice') + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + @serializable.xml_name('slice') + def slice_(self) -> Optional[str]: + return self._slice + + @slice_.setter + def slice_(self, slice_: Optional[str]) -> None: + self._slice = slice_ + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(4) + @serializable.json_name('confidenceInterval') + @serializable.xml_name('confidenceInterval') + def confidence_interval(self) -> Optional[ConfidenceInterval]: + return self._confidence_interval + + @confidence_interval.setter + def confidence_interval(self, confidence_interval: Optional[ConfidenceInterval]) -> None: + self._confidence_interval = confidence_interval + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((self.type, self.value, self.slice_, self.confidence_interval)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, PerformanceMetric): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, PerformanceMetric): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, PerformanceMetric): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, PerformanceMetric): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class Graphic: + """Graphic entry with optional name and image (AttachedText).""" + + def __init__(self, *, name: Optional[str] = None, image: Optional[AttachedText] = None) -> None: + self.name = name + self.image = image + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def name(self) -> Optional[str]: + return self._name + + @name.setter + def name(self, name: Optional[str]) -> None: + self._name = name + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + def image(self) -> Optional[AttachedText]: + return self._image + + @image.setter + def image(self, image: Optional[AttachedText]) -> None: + self._image = image + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((self.name, self.image)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Graphic): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Graphic): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, Graphic): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, Graphic): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class GraphicsCollection: + """A collection of graphics with optional description.""" + + def __init__(self, *, description: Optional[str] = None, collection: Optional[Iterable[Graphic]] = None) -> None: + self.description = description + self.collection = collection or [] + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def description(self) -> Optional[str]: + return self._description + + @description.setter + def description(self, description: Optional[str]) -> None: + self._description = description + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + @serializable.json_name('collection') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'graphic') + @serializable.xml_name('collection') + def collection(self) -> 'SortedSet[Graphic]': + return self._collection + + @collection.setter + def collection(self, collection: Iterable[Graphic]) -> None: + self._collection = SortedSet(collection) + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((self.description, _ComparableTuple(self.collection))) + + def __eq__(self, other: object) -> bool: + if isinstance(other, GraphicsCollection): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, GraphicsCollection): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, GraphicsCollection): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, GraphicsCollection): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class QuantitativeAnalysis: + """`quantitativeAnalysis` block within `modelCard`.""" + + def __init__( + self, *, + performance_metrics: Optional[Iterable[PerformanceMetric]] = None, + graphics: Optional[GraphicsCollection] = None, + ) -> None: + self.performance_metrics = performance_metrics or [] + self.graphics = graphics + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + @serializable.json_name('performanceMetrics') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'performanceMetric') + @serializable.xml_name('performanceMetrics') + def performance_metrics(self) -> 'SortedSet[PerformanceMetric]': + return self._performance_metrics + + @performance_metrics.setter + def performance_metrics(self, performance_metrics: Iterable[PerformanceMetric]) -> None: + self._performance_metrics = SortedSet(performance_metrics) + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + def graphics(self) -> Optional[GraphicsCollection]: + return self._graphics + + @graphics.setter + def graphics(self, graphics: Optional[GraphicsCollection]) -> None: + self._graphics = graphics + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((_ComparableTuple(self.performance_metrics), self.graphics)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, QuantitativeAnalysis): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, QuantitativeAnalysis): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, QuantitativeAnalysis): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, QuantitativeAnalysis): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +# Considerations and nested structures + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class EthicalConsideration: + """Entry in `ethicalConsiderations` with name and mitigation strategy.""" + + def __init__(self, *, name: Optional[str] = None, mitigation_strategy: Optional[str] = None) -> None: + self.name = name + self.mitigation_strategy = mitigation_strategy + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def name(self) -> Optional[str]: + return self._name + + @name.setter + def name(self, name: Optional[str]) -> None: + self._name = name + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + @serializable.json_name('mitigationStrategy') + @serializable.xml_name('mitigationStrategy') + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def mitigation_strategy(self) -> Optional[str]: + return self._mitigation_strategy + + @mitigation_strategy.setter + def mitigation_strategy(self, mitigation_strategy: Optional[str]) -> None: + self._mitigation_strategy = mitigation_strategy + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((self.name, self.mitigation_strategy)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, EthicalConsideration): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, EthicalConsideration): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, EthicalConsideration): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, EthicalConsideration): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class FairnessAssessment: + """Entry in `fairnessAssessments`.""" + + def __init__( + self, *, + group_at_risk: Optional[str] = None, + benefits: Optional[str] = None, + harms: Optional[str] = None, + mitigation_strategy: Optional[str] = None, + ) -> None: + self.group_at_risk = group_at_risk + self.benefits = benefits + self.harms = harms + self.mitigation_strategy = mitigation_strategy + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + @serializable.json_name('groupAtRisk') + @serializable.xml_name('groupAtRisk') + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def group_at_risk(self) -> Optional[str]: + return self._group_at_risk + + @group_at_risk.setter + def group_at_risk(self, group_at_risk: Optional[str]) -> None: + self._group_at_risk = group_at_risk + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def benefits(self) -> Optional[str]: + return self._benefits + + @benefits.setter + def benefits(self, benefits: Optional[str]) -> None: + self._benefits = benefits + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(3) + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def harms(self) -> Optional[str]: + return self._harms + + @harms.setter + def harms(self, harms: Optional[str]) -> None: + self._harms = harms + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(4) + @serializable.json_name('mitigationStrategy') + @serializable.xml_name('mitigationStrategy') + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def mitigation_strategy(self) -> Optional[str]: + return self._mitigation_strategy + + @mitigation_strategy.setter + def mitigation_strategy(self, mitigation_strategy: Optional[str]) -> None: + self._mitigation_strategy = mitigation_strategy + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((self.group_at_risk, self.benefits, self.harms, self.mitigation_strategy)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, FairnessAssessment): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, FairnessAssessment): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, FairnessAssessment): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, FairnessAssessment): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class EnvironmentalConsiderations: + """Environmental considerations (1.6+). Energy consumptions and properties. + + NOTE: Prior revisions kept `energy_consumptions` opaque. This has been replaced by + concrete types that match CycloneDX 1.6+/1.7 schema: `EnergyConsumption`, `EnergyMeasure`, + `Co2Measure`, `EnergyProvider`, and enumerations for `activity` and `energySource`. + """ + + def __init__( + self, *, + energy_consumptions: Optional[Iterable['EnergyConsumption']] = None, + properties: Optional[Iterable[Property]] = None, + ) -> None: + self.energy_consumptions = energy_consumptions or [] + self.properties = properties or [] + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + @serializable.json_name('energyConsumptions') + @serializable.xml_name('energyConsumptions') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'energyConsumption') + def energy_consumptions(self) -> 'SortedSet[EnergyConsumption]': + return self._energy_consumptions + + @energy_consumptions.setter + def energy_consumptions(self, energy_consumptions: Iterable['EnergyConsumption']) -> None: + self._energy_consumptions = SortedSet(energy_consumptions) + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + @serializable.xml_name('properties') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property') + def properties(self) -> 'SortedSet[Property]': + return self._properties + + @properties.setter + def properties(self, properties: Iterable[Property]) -> None: + self._properties = SortedSet(properties) + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((_ComparableTuple(self.energy_consumptions), _ComparableTuple(self.properties))) + + def __eq__(self, other: object) -> bool: + if isinstance(other, EnvironmentalConsiderations): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, EnvironmentalConsiderations): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, EnvironmentalConsiderations): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, EnvironmentalConsiderations): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_enum +class EnergyActivity(str, Enum): + """Enumeration for lifecycle activity in `energyConsumption.activity` (1.6+).""" + DESIGN = 'design' + DATA_COLLECTION = 'data-collection' + DATA_PREPARATION = 'data-preparation' + TRAINING = 'training' + FINE_TUNING = 'fine-tuning' + VALIDATION = 'validation' + DEPLOYMENT = 'deployment' + INFERENCE = 'inference' + OTHER = 'other' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class EnergyMeasure: + """A measure of energy. Schema `energyMeasure` (1.6+): value + unit (kWh).""" + + def __init__(self, *, value: float, unit: EnergyMeasureUnit = EnergyMeasureUnit.KWH) -> None: + self.value = value + self.unit = unit + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + def value(self) -> float: + return self._value + + @value.setter + def value(self, value: float) -> None: + self._value = value + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + def unit(self) -> EnergyMeasureUnit: + return self._unit + + @unit.setter + def unit(self, unit: EnergyMeasureUnit) -> None: + self._unit = unit + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((self.value, self.unit)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, EnergyMeasure): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, EnergyMeasure): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, EnergyMeasure): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, EnergyMeasure): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class Co2Measure: + """A measure of CO2. Schema `co2Measure` (1.6+): value + unit (tCO2eq).""" + + def __init__(self, *, value: float, unit: Co2MeasureUnit = Co2MeasureUnit.TCO2EQ) -> None: + self.value = value + self.unit = unit + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + def value(self) -> float: + return self._value + + @value.setter + def value(self, value: float) -> None: + self._value = value + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + def unit(self) -> Co2MeasureUnit: + return self._unit + + @unit.setter + def unit(self, unit: Co2MeasureUnit) -> None: + self._unit = unit + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple((self.value, self.unit)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Co2Measure): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Co2Measure): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, Co2Measure): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, Co2Measure): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_enum +class EnergySource(str, Enum): + """Enumeration for provider `energySource` (1.6+).""" + COAL = 'coal' + OIL = 'oil' + NATURAL_GAS = 'natural-gas' + NUCLEAR = 'nuclear' + WIND = 'wind' + SOLAR = 'solar' + GEOTHERMAL = 'geothermal' + HYDROPOWER = 'hydropower' + BIOFUEL = 'biofuel' + UNKNOWN = 'unknown' + OTHER = 'other' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class EnergyProvider: + """Energy provider per schema `energyProvider` (1.6+).""" + + def __init__( + self, *, + organization: OrganizationalEntity, + energy_source: EnergySource, + energy_provided: EnergyMeasure, + bom_ref: Optional[Union[str, BomRef]] = None, + description: Optional[str] = None, + external_references: Optional[Iterable[ExternalReference]] = None, + ) -> None: + self._bom_ref = _bom_ref_from_str(bom_ref) if bom_ref is not None else _bom_ref_from_str(None) + self.description = description + self.organization = organization + self.energy_source = energy_source + self.energy_provided = energy_provided + self.external_references = external_references or [] + + @property + @serializable.json_name('bom-ref') + @serializable.type_mapping(BomRef) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_attribute() + @serializable.xml_name('bom-ref') + def bom_ref(self) -> BomRef: + return self._bom_ref + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def description(self) -> Optional[str]: + return self._description + + @description.setter + def description(self, description: Optional[str]) -> None: + self._description = description + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + def organization(self) -> OrganizationalEntity: + return self._organization + + @organization.setter + def organization(self, organization: OrganizationalEntity) -> None: + self._organization = organization + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(3) + @serializable.json_name('energySource') + @serializable.xml_name('energySource') + def energy_source(self) -> EnergySource: + return self._energy_source + + @energy_source.setter + def energy_source(self, energy_source: EnergySource) -> None: + self._energy_source = energy_source + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(4) + @serializable.json_name('energyProvided') + @serializable.xml_name('energyProvided') + def energy_provided(self) -> EnergyMeasure: + return self._energy_provided + + @energy_provided.setter + def energy_provided(self, energy_provided: EnergyMeasure) -> None: + self._energy_provided = energy_provided + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(5) + @serializable.json_name('externalReferences') + @serializable.xml_name('externalReferences') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference') + def external_references(self) -> 'SortedSet[ExternalReference]': + return self._external_references + + @external_references.setter + def external_references(self, external_references: Iterable[ExternalReference]) -> None: + self._external_references = SortedSet(external_references) + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self._bom_ref.value, + self.description, + self.organization, + self.energy_source, + self.energy_provided, + _ComparableTuple(self.external_references), + )) + + def __eq__(self, other: object) -> bool: + if isinstance(other, EnergyProvider): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, EnergyProvider): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, EnergyProvider): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, EnergyProvider): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class EnergyConsumption: + """Energy consumption entry. Matches schema `energyConsumption` (1.6+).""" + + def __init__( + self, *, + activity: EnergyActivity, + energy_providers: Iterable[EnergyProvider], + activity_energy_cost: EnergyMeasure, + co2_cost_equivalent: Optional[Co2Measure] = None, + co2_cost_offset: Optional[Co2Measure] = None, + properties: Optional[Iterable[Property]] = None, + ) -> None: + self.activity = activity + self.energy_providers = energy_providers + self.activity_energy_cost = activity_energy_cost + self.co2_cost_equivalent = co2_cost_equivalent + self.co2_cost_offset = co2_cost_offset + self.properties = properties or [] + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + def activity(self) -> EnergyActivity: + return self._activity + + @activity.setter + def activity(self, activity: EnergyActivity) -> None: + self._activity = activity + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + @serializable.json_name('energyProviders') + @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'energyProviders') + def energy_providers(self) -> 'SortedSet[EnergyProvider]': + return self._energy_providers + + @energy_providers.setter + def energy_providers(self, energy_providers: Iterable[EnergyProvider]) -> None: + self._energy_providers = SortedSet(energy_providers) + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(3) + @serializable.json_name('activityEnergyCost') + @serializable.xml_name('activityEnergyCost') + def activity_energy_cost(self) -> EnergyMeasure: + return self._activity_energy_cost + + @activity_energy_cost.setter + def activity_energy_cost(self, activity_energy_cost: EnergyMeasure) -> None: + self._activity_energy_cost = activity_energy_cost + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(4) + @serializable.json_name('co2CostEquivalent') + @serializable.xml_name('co2CostEquivalent') + def co2_cost_equivalent(self) -> Optional[Co2Measure]: + return self._co2_cost_equivalent + + @co2_cost_equivalent.setter + def co2_cost_equivalent(self, co2_cost_equivalent: Optional[Co2Measure]) -> None: + self._co2_cost_equivalent = co2_cost_equivalent + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(5) + @serializable.json_name('co2CostOffset') + @serializable.xml_name('co2CostOffset') + def co2_cost_offset(self) -> Optional[Co2Measure]: + return self._co2_cost_offset + + @co2_cost_offset.setter + def co2_cost_offset(self, co2_cost_offset: Optional[Co2Measure]) -> None: + self._co2_cost_offset = co2_cost_offset + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(6) + @serializable.xml_name('properties') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property') + def properties(self) -> 'SortedSet[Property]': + return self._properties + + @properties.setter + def properties(self, properties: Iterable[Property]) -> None: + self._properties = SortedSet(properties) + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.activity, + _ComparableTuple(self.energy_providers), + self.activity_energy_cost, + self.co2_cost_equivalent, + self.co2_cost_offset, + _ComparableTuple(self.properties), + )) + + def __eq__(self, other: object) -> bool: + if isinstance(other, EnergyConsumption): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, EnergyConsumption): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, EnergyConsumption): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, EnergyConsumption): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return ( + f'' + ) + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class Considerations: + """`considerations` block within `modelCard`.""" + + def __init__( + self, *, + users: Optional[Iterable[str]] = None, + use_cases: Optional[Iterable[str]] = None, + technical_limitations: Optional[Iterable[str]] = None, + performance_tradeoffs: Optional[Iterable[str]] = None, + ethical_considerations: Optional[Iterable[EthicalConsideration]] = None, + environmental_considerations: Optional[EnvironmentalConsiderations] = None, + fairness_assessments: Optional[Iterable[FairnessAssessment]] = None, + ) -> None: + self.users = users or [] + self.use_cases = use_cases or [] + self.technical_limitations = technical_limitations or [] + self.performance_tradeoffs = performance_tradeoffs or [] + self.ethical_considerations = ethical_considerations or [] + self.environmental_considerations = environmental_considerations + self.fairness_assessments = fairness_assessments or [] + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + @serializable.xml_name('users') + @serializable.json_name('users') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'user') + def users(self) -> 'SortedSet[str]': + return self._users + + @users.setter + def users(self, users: Iterable[str]) -> None: + self._users = SortedSet(users) + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + @serializable.json_name('useCases') + @serializable.xml_name('useCases') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'useCase') + def use_cases(self) -> 'SortedSet[str]': + return self._use_cases + + @use_cases.setter + def use_cases(self, use_cases: Iterable[str]) -> None: + self._use_cases = SortedSet(use_cases) + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(3) + @serializable.json_name('technicalLimitations') + @serializable.xml_name('technicalLimitations') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'technicalLimitation') + def technical_limitations(self) -> 'SortedSet[str]': + return self._technical_limitations + + @technical_limitations.setter + def technical_limitations(self, technical_limitations: Iterable[str]) -> None: + self._technical_limitations = SortedSet(technical_limitations) + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(4) + @serializable.json_name('performanceTradeoffs') + @serializable.xml_name('performanceTradeoffs') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'performanceTradeoff') + def performance_tradeoffs(self) -> 'SortedSet[str]': + return self._performance_tradeoffs + + @performance_tradeoffs.setter + def performance_tradeoffs(self, performance_tradeoffs: Iterable[str]) -> None: + self._performance_tradeoffs = SortedSet(performance_tradeoffs) + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(5) + @serializable.json_name('ethicalConsiderations') + @serializable.xml_name('ethicalConsiderations') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'ethicalConsideration') + def ethical_considerations(self) -> 'SortedSet[EthicalConsideration]': + return self._ethical_considerations + + @ethical_considerations.setter + def ethical_considerations(self, ethical_considerations: Iterable[EthicalConsideration]) -> None: + self._ethical_considerations = SortedSet(ethical_considerations) + + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(6) + @serializable.json_name('environmentalConsiderations') + @serializable.xml_name('environmentalConsiderations') + def environmental_considerations(self) -> Optional[EnvironmentalConsiderations]: + return self._environmental_considerations + + @environmental_considerations.setter + def environmental_considerations(self, environmental_considerations: Optional[EnvironmentalConsiderations]) -> None: + self._environmental_considerations = environmental_considerations + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(7) + @serializable.json_name('fairnessAssessments') + @serializable.xml_name('fairnessAssessments') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'fairnessAssessment') + def fairness_assessments(self) -> 'SortedSet[FairnessAssessment]': + return self._fairness_assessments + + @fairness_assessments.setter + def fairness_assessments(self, fairness_assessments: Iterable[FairnessAssessment]) -> None: + self._fairness_assessments = SortedSet(fairness_assessments) + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + _ComparableTuple(self.users), + _ComparableTuple(self.use_cases), + _ComparableTuple(self.technical_limitations), + _ComparableTuple(self.performance_tradeoffs), + _ComparableTuple(self.ethical_considerations), + self.environmental_considerations, + _ComparableTuple(self.fairness_assessments), + )) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Considerations): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Considerations): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, Considerations): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, Considerations): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return ( + f'' + ) + + +@serializable.serializable_class(ignore_unknown_during_deserialization=True) +class ModelCard: + """Internal representation of CycloneDX `modelCardType`. + + Version gating: + - Introduced in schema 1.5 + - Unchanged structurally in 1.6 except for additional nested environmental considerations inside `considerations` + - 1.7 retains 1.6 structure (additions in nested types only) + """ + + def __init__( + self, *, + bom_ref: Optional[Union[str, BomRef]] = None, + model_parameters: Optional[ModelParameters] = None, + quantitative_analysis: Optional[QuantitativeAnalysis] = None, + considerations: Optional[Considerations] = None, + properties: Optional[Iterable[Property]] = None, + ) -> None: + self._bom_ref = _bom_ref_from_str(bom_ref) if bom_ref is not None else _bom_ref_from_str(None) + self.model_parameters = model_parameters + self.quantitative_analysis = quantitative_analysis + self.considerations = considerations + self.properties = properties or [] + + # bom-ref attribute + @property + @serializable.json_name('bom-ref') + @serializable.type_mapping(BomRef) + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_attribute() + @serializable.xml_name('bom-ref') + def bom_ref(self) -> BomRef: + return self._bom_ref + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(1) + @serializable.json_name('modelParameters') + @serializable.xml_name('modelParameters') + def model_parameters(self) -> Optional[ModelParameters]: + return self._model_parameters + + @model_parameters.setter + def model_parameters(self, model_parameters: Optional[ModelParameters]) -> None: + self._model_parameters = model_parameters + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(2) + @serializable.json_name('quantitativeAnalysis') + @serializable.xml_name('quantitativeAnalysis') + def quantitative_analysis(self) -> Optional[QuantitativeAnalysis]: + return self._quantitative_analysis + + @quantitative_analysis.setter + def quantitative_analysis(self, quantitative_analysis: Optional[QuantitativeAnalysis]) -> None: + self._quantitative_analysis = quantitative_analysis + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(3) + @serializable.json_name('considerations') + @serializable.xml_name('considerations') + def considerations(self) -> Optional[Considerations]: + return self._considerations + + @considerations.setter + def considerations(self, considerations: Optional[Considerations]) -> None: + self._considerations = considerations + + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_sequence(4) + @serializable.json_name('properties') + @serializable.xml_name('properties') + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property') + def properties(self) -> 'SortedSet[Property]': + return self._properties + + @properties.setter + def properties(self, properties: Iterable[Property]) -> None: + self._properties = SortedSet(properties) + + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.bom_ref.value, + self.model_parameters, + self.quantitative_analysis, + self.considerations, + _ComparableTuple(self.properties), + )) + + def __eq__(self, other: object) -> bool: + if isinstance(other, ModelCard): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, ModelCard): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __le__(self, other: Any) -> bool: + if isinstance(other, ModelCard): + return self.__comparable_tuple() <= other.__comparable_tuple() + return NotImplemented + + def __ge__(self, other: Any) -> bool: + if isinstance(other, ModelCard): + return self.__comparable_tuple() >= other.__comparable_tuple() + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' diff --git a/tests/_data/models.py b/tests/_data/models.py index 565f56ec..4e42c4a9 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -105,6 +105,27 @@ LicenseExpressionDetails, ) from cyclonedx.model.lifecycle import LifecyclePhase, NamedLifecycle, PredefinedLifecycle +from cyclonedx.model.model_card import ( + Approach, + Co2Measure, + Considerations, + EnergyActivity, + EnergyConsumption, + EnergyMeasure, + EnergyProvider, + EnergySource, + EnvironmentalConsiderations, + EthicalConsideration, + FairnessAssessment, + Graphic, + GraphicsCollection, + InputOutputMLParameters, + MachineLearningApproach, + ModelCard, + ModelParameters, + PerformanceMetric, + QuantitativeAnalysis, +) from cyclonedx.model.release_note import ReleaseNotes from cyclonedx.model.service import Service from cyclonedx.model.tool import Tool, ToolRepository @@ -411,6 +432,120 @@ def get_bom_with_component_setuptools_with_release_notes() -> Bom: return _make_bom(components=[component]) +def get_bom_v1_5_with_full_model_card() -> Bom: + """Single ML component with a fully populated modelCard (schema 1.5+ only).""" + + image_content = base64.b64encode(b'full-model-card-image').decode('utf-8') + + provider = EnergyProvider( + bom_ref='mc-energy-provider', + description='Primary renewable provider', + organization=get_org_entity_1(), + energy_source=EnergySource.WIND, + energy_provided=EnergyMeasure(value=321.0), + external_references=[get_external_reference_1()], + ) + + env = EnvironmentalConsiderations( + energy_consumptions=[ + EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[provider], + activity_energy_cost=EnergyMeasure(value=42.0), + co2_cost_equivalent=Co2Measure(value=0.7), + co2_cost_offset=Co2Measure(value=0.2), + properties=[Property(name='phase', value='pretraining')], + ) + ], + properties=[Property(name='footprint', value='low')], + ) + + model_card = ModelCard( + bom_ref='mc-full', + model_parameters=ModelParameters( + approach=Approach(type=MachineLearningApproach.SUPERVISED), + task='text-classification', + architecture_family='Transformer', + model_architecture='tiny-transformer-v1', + inputs=[ + InputOutputMLParameters(format='text/plain'), + InputOutputMLParameters(format='application/json'), + ], + outputs=[ + InputOutputMLParameters(format='label'), + InputOutputMLParameters(format='confidence-score'), + ], + ), + quantitative_analysis=QuantitativeAnalysis( + performance_metrics=[ + PerformanceMetric(type='f1', value='0.88', slice_='en'), + PerformanceMetric(type='accuracy', value='0.93', slice_='all'), + ], + graphics=GraphicsCollection( + description='Performance plots', + collection=[ + Graphic( + name='roc-curve', + image=AttachedText( + content=image_content, + content_type='image/png', + encoding=Encoding.BASE_64, + ), + ), + Graphic( + name='pr-curve', + image=AttachedText( + content=image_content, + content_type='image/png', + encoding=Encoding.BASE_64, + ), + ), + ], + ), + ), + considerations=Considerations( + users=['ml-engineer', 'data-scientist'], + use_cases=['spam-detection', 'content-moderation'], + technical_limitations=['may misclassify non-standard language', 'limited to 512 tokens'], + performance_tradeoffs=['accuracy-over-speed', 'high-memory-footprint'], + ethical_considerations=[ + EthicalConsideration( + name='privacy', mitigation_strategy='anonymize inputs and strip PII' + ), + EthicalConsideration( + name='bias', mitigation_strategy='augment data and monitor drift' + ), + ], + environmental_considerations=env, + fairness_assessments=[ + FairnessAssessment( + group_at_risk='non-native speakers', + benefits='faster responses', + harms='potential false positives', + mitigation_strategy='rebalance dataset and track metrics', + ), + ], + ), + properties=[ + Property(name='release', value='2024-01-01'), + Property(name='owner', value='ml-team'), + ], + ) + + component = Component( + name='full-model-card-component', + version='1.0.0', + type=ComponentType.MACHINE_LEARNING_MODEL, + bom_ref='ml-card-full', + purl=PackageURL(type='pypi', name='full-model-card', version='1.0.0'), + model_card=model_card, + ) + + bom = _make_bom(components=[component]) + bom.register_dependency(component) + return bom + + def get_bom_with_dependencies_valid() -> Bom: c1 = get_component_setuptools_simple() c2 = get_component_toml_with_hashes_with_references() diff --git a/tests/_data/snapshots/enum_Co2MeasureUnit-1.6.json.bin b/tests/_data/snapshots/enum_Co2MeasureUnit-1.6.json.bin new file mode 100644 index 00000000..ede931fa --- /dev/null +++ b/tests/_data/snapshots/enum_Co2MeasureUnit-1.6.json.bin @@ -0,0 +1,63 @@ +{ + "components": [ + { + "bom-ref": "dummy-CMU:TCO2EQ", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "co2CostEquivalent": { + "unit": "tCO2eq", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "Co2MeasureUnit: TCO2EQ", + "type": "machine-learning-model" + } + ], + "dependencies": [ + { + "ref": "dummy-CMU:TCO2EQ" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_Co2MeasureUnit-1.6.xml.bin b/tests/_data/snapshots/enum_Co2MeasureUnit-1.6.xml.bin new file mode 100644 index 00000000..07bb3b3d --- /dev/null +++ b/tests/_data/snapshots/enum_Co2MeasureUnit-1.6.xml.bin @@ -0,0 +1,47 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + Co2MeasureUnit: TCO2EQ + + + + + + training + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + 1.0 + tCO2eq + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/enum_Co2MeasureUnit-1.7.json.bin b/tests/_data/snapshots/enum_Co2MeasureUnit-1.7.json.bin new file mode 100644 index 00000000..6bfa0d24 --- /dev/null +++ b/tests/_data/snapshots/enum_Co2MeasureUnit-1.7.json.bin @@ -0,0 +1,63 @@ +{ + "components": [ + { + "bom-ref": "dummy-CMU:TCO2EQ", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "co2CostEquivalent": { + "unit": "tCO2eq", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "Co2MeasureUnit: TCO2EQ", + "type": "machine-learning-model" + } + ], + "dependencies": [ + { + "ref": "dummy-CMU:TCO2EQ" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_Co2MeasureUnit-1.7.xml.bin b/tests/_data/snapshots/enum_Co2MeasureUnit-1.7.xml.bin new file mode 100644 index 00000000..91ef38a1 --- /dev/null +++ b/tests/_data/snapshots/enum_Co2MeasureUnit-1.7.xml.bin @@ -0,0 +1,47 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + Co2MeasureUnit: TCO2EQ + + + + + + training + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + 1.0 + tCO2eq + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/enum_EnergyActivity-1.6.json.bin b/tests/_data/snapshots/enum_EnergyActivity-1.6.json.bin new file mode 100644 index 00000000..d88bdbca --- /dev/null +++ b/tests/_data/snapshots/enum_EnergyActivity-1.6.json.bin @@ -0,0 +1,339 @@ +{ + "components": [ + { + "bom-ref": "dummy-EA:DATA_COLLECTION", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "data-collection", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: DATA_COLLECTION", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:DATA_PREPARATION", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "data-preparation", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: DATA_PREPARATION", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:DEPLOYMENT", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "deployment", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: DEPLOYMENT", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:DESIGN", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "design", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: DESIGN", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:FINE_TUNING", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "fine-tuning", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: FINE_TUNING", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:INFERENCE", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "inference", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: INFERENCE", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:OTHER", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "other", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: OTHER", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:TRAINING", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: TRAINING", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:VALIDATION", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "validation", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: VALIDATION", + "type": "machine-learning-model" + } + ], + "dependencies": [ + { + "ref": "dummy-EA:DATA_COLLECTION" + }, + { + "ref": "dummy-EA:DATA_PREPARATION" + }, + { + "ref": "dummy-EA:DEPLOYMENT" + }, + { + "ref": "dummy-EA:DESIGN" + }, + { + "ref": "dummy-EA:FINE_TUNING" + }, + { + "ref": "dummy-EA:INFERENCE" + }, + { + "ref": "dummy-EA:OTHER" + }, + { + "ref": "dummy-EA:TRAINING" + }, + { + "ref": "dummy-EA:VALIDATION" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_EnergyActivity-1.6.xml.bin b/tests/_data/snapshots/enum_EnergyActivity-1.6.xml.bin new file mode 100644 index 00000000..0e4d5efa --- /dev/null +++ b/tests/_data/snapshots/enum_EnergyActivity-1.6.xml.bin @@ -0,0 +1,275 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + EnergyActivity: DATA_COLLECTION + + + + + + data-collection + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: DATA_PREPARATION + + + + + + data-preparation + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: DEPLOYMENT + + + + + + deployment + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: DESIGN + + + + + + design + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: FINE_TUNING + + + + + + fine-tuning + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: INFERENCE + + + + + + inference + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: OTHER + + + + + + other + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: TRAINING + + + + + + training + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: VALIDATION + + + + + + validation + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/enum_EnergyActivity-1.7.json.bin b/tests/_data/snapshots/enum_EnergyActivity-1.7.json.bin new file mode 100644 index 00000000..5798fa39 --- /dev/null +++ b/tests/_data/snapshots/enum_EnergyActivity-1.7.json.bin @@ -0,0 +1,339 @@ +{ + "components": [ + { + "bom-ref": "dummy-EA:DATA_COLLECTION", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "data-collection", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: DATA_COLLECTION", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:DATA_PREPARATION", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "data-preparation", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: DATA_PREPARATION", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:DEPLOYMENT", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "deployment", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: DEPLOYMENT", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:DESIGN", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "design", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: DESIGN", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:FINE_TUNING", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "fine-tuning", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: FINE_TUNING", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:INFERENCE", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "inference", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: INFERENCE", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:OTHER", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "other", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: OTHER", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:TRAINING", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: TRAINING", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-EA:VALIDATION", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "validation", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyActivity: VALIDATION", + "type": "machine-learning-model" + } + ], + "dependencies": [ + { + "ref": "dummy-EA:DATA_COLLECTION" + }, + { + "ref": "dummy-EA:DATA_PREPARATION" + }, + { + "ref": "dummy-EA:DEPLOYMENT" + }, + { + "ref": "dummy-EA:DESIGN" + }, + { + "ref": "dummy-EA:FINE_TUNING" + }, + { + "ref": "dummy-EA:INFERENCE" + }, + { + "ref": "dummy-EA:OTHER" + }, + { + "ref": "dummy-EA:TRAINING" + }, + { + "ref": "dummy-EA:VALIDATION" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_EnergyActivity-1.7.xml.bin b/tests/_data/snapshots/enum_EnergyActivity-1.7.xml.bin new file mode 100644 index 00000000..5b0df586 --- /dev/null +++ b/tests/_data/snapshots/enum_EnergyActivity-1.7.xml.bin @@ -0,0 +1,275 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + EnergyActivity: DATA_COLLECTION + + + + + + data-collection + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: DATA_PREPARATION + + + + + + data-preparation + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: DEPLOYMENT + + + + + + deployment + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: DESIGN + + + + + + design + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: FINE_TUNING + + + + + + fine-tuning + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: INFERENCE + + + + + + inference + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: OTHER + + + + + + other + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: TRAINING + + + + + + training + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergyActivity: VALIDATION + + + + + + validation + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/enum_EnergyMeasureUnit-1.6.json.bin b/tests/_data/snapshots/enum_EnergyMeasureUnit-1.6.json.bin new file mode 100644 index 00000000..f57a71e6 --- /dev/null +++ b/tests/_data/snapshots/enum_EnergyMeasureUnit-1.6.json.bin @@ -0,0 +1,59 @@ +{ + "components": [ + { + "bom-ref": "dummy-EMU:KWH", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyMeasureUnit: KWH", + "type": "machine-learning-model" + } + ], + "dependencies": [ + { + "ref": "dummy-EMU:KWH" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_EnergyMeasureUnit-1.6.xml.bin b/tests/_data/snapshots/enum_EnergyMeasureUnit-1.6.xml.bin new file mode 100644 index 00000000..00efa83e --- /dev/null +++ b/tests/_data/snapshots/enum_EnergyMeasureUnit-1.6.xml.bin @@ -0,0 +1,43 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + EnergyMeasureUnit: KWH + + + + + + training + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/enum_EnergyMeasureUnit-1.7.json.bin b/tests/_data/snapshots/enum_EnergyMeasureUnit-1.7.json.bin new file mode 100644 index 00000000..865324be --- /dev/null +++ b/tests/_data/snapshots/enum_EnergyMeasureUnit-1.7.json.bin @@ -0,0 +1,59 @@ +{ + "components": [ + { + "bom-ref": "dummy-EMU:KWH", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergyMeasureUnit: KWH", + "type": "machine-learning-model" + } + ], + "dependencies": [ + { + "ref": "dummy-EMU:KWH" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_EnergyMeasureUnit-1.7.xml.bin b/tests/_data/snapshots/enum_EnergyMeasureUnit-1.7.xml.bin new file mode 100644 index 00000000..0cdbf51d --- /dev/null +++ b/tests/_data/snapshots/enum_EnergyMeasureUnit-1.7.xml.bin @@ -0,0 +1,43 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + EnergyMeasureUnit: KWH + + + + + + training + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/enum_EnergySource-1.6.json.bin b/tests/_data/snapshots/enum_EnergySource-1.6.json.bin new file mode 100644 index 00000000..58c7a2c9 --- /dev/null +++ b/tests/_data/snapshots/enum_EnergySource-1.6.json.bin @@ -0,0 +1,409 @@ +{ + "components": [ + { + "bom-ref": "dummy-ES:BIOFUEL", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "biofuel", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: BIOFUEL", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:COAL", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "coal", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: COAL", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:GEOTHERMAL", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "geothermal", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: GEOTHERMAL", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:HYDROPOWER", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "hydropower", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: HYDROPOWER", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:NATURAL_GAS", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "natural-gas", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: NATURAL_GAS", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:NUCLEAR", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "nuclear", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: NUCLEAR", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:OIL", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "oil", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: OIL", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:OTHER", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "other", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: OTHER", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:SOLAR", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: SOLAR", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:UNKNOWN", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "unknown", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: UNKNOWN", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:WIND", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "wind", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: WIND", + "type": "machine-learning-model" + } + ], + "dependencies": [ + { + "ref": "dummy-ES:BIOFUEL" + }, + { + "ref": "dummy-ES:COAL" + }, + { + "ref": "dummy-ES:GEOTHERMAL" + }, + { + "ref": "dummy-ES:HYDROPOWER" + }, + { + "ref": "dummy-ES:NATURAL_GAS" + }, + { + "ref": "dummy-ES:NUCLEAR" + }, + { + "ref": "dummy-ES:OIL" + }, + { + "ref": "dummy-ES:OTHER" + }, + { + "ref": "dummy-ES:SOLAR" + }, + { + "ref": "dummy-ES:UNKNOWN" + }, + { + "ref": "dummy-ES:WIND" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_EnergySource-1.6.xml.bin b/tests/_data/snapshots/enum_EnergySource-1.6.xml.bin new file mode 100644 index 00000000..941c773a --- /dev/null +++ b/tests/_data/snapshots/enum_EnergySource-1.6.xml.bin @@ -0,0 +1,333 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + EnergySource: BIOFUEL + + + + + + training + + + test-org + + biofuel + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: COAL + + + + + + training + + + test-org + + coal + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: GEOTHERMAL + + + + + + training + + + test-org + + geothermal + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: HYDROPOWER + + + + + + training + + + test-org + + hydropower + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: NATURAL_GAS + + + + + + training + + + test-org + + natural-gas + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: NUCLEAR + + + + + + training + + + test-org + + nuclear + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: OIL + + + + + + training + + + test-org + + oil + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: OTHER + + + + + + training + + + test-org + + other + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: SOLAR + + + + + + training + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: UNKNOWN + + + + + + training + + + test-org + + unknown + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: WIND + + + + + + training + + + test-org + + wind + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + + + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/enum_EnergySource-1.7.json.bin b/tests/_data/snapshots/enum_EnergySource-1.7.json.bin new file mode 100644 index 00000000..f0f01516 --- /dev/null +++ b/tests/_data/snapshots/enum_EnergySource-1.7.json.bin @@ -0,0 +1,409 @@ +{ + "components": [ + { + "bom-ref": "dummy-ES:BIOFUEL", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "biofuel", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: BIOFUEL", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:COAL", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "coal", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: COAL", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:GEOTHERMAL", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "geothermal", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: GEOTHERMAL", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:HYDROPOWER", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "hydropower", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: HYDROPOWER", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:NATURAL_GAS", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "natural-gas", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: NATURAL_GAS", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:NUCLEAR", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "nuclear", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: NUCLEAR", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:OIL", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "oil", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: OIL", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:OTHER", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "other", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: OTHER", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:SOLAR", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "solar", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: SOLAR", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:UNKNOWN", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "unknown", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: UNKNOWN", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-ES:WIND", + "modelCard": { + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 1.0 + }, + "energyProviders": [ + { + "energyProvided": { + "unit": "kWh", + "value": 1.0 + }, + "energySource": "wind", + "organization": { + "name": "test-org" + } + } + ] + } + ] + } + } + }, + "name": "EnergySource: WIND", + "type": "machine-learning-model" + } + ], + "dependencies": [ + { + "ref": "dummy-ES:BIOFUEL" + }, + { + "ref": "dummy-ES:COAL" + }, + { + "ref": "dummy-ES:GEOTHERMAL" + }, + { + "ref": "dummy-ES:HYDROPOWER" + }, + { + "ref": "dummy-ES:NATURAL_GAS" + }, + { + "ref": "dummy-ES:NUCLEAR" + }, + { + "ref": "dummy-ES:OIL" + }, + { + "ref": "dummy-ES:OTHER" + }, + { + "ref": "dummy-ES:SOLAR" + }, + { + "ref": "dummy-ES:UNKNOWN" + }, + { + "ref": "dummy-ES:WIND" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_EnergySource-1.7.xml.bin b/tests/_data/snapshots/enum_EnergySource-1.7.xml.bin new file mode 100644 index 00000000..05e8444f --- /dev/null +++ b/tests/_data/snapshots/enum_EnergySource-1.7.xml.bin @@ -0,0 +1,333 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + EnergySource: BIOFUEL + + + + + + training + + + test-org + + biofuel + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: COAL + + + + + + training + + + test-org + + coal + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: GEOTHERMAL + + + + + + training + + + test-org + + geothermal + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: HYDROPOWER + + + + + + training + + + test-org + + hydropower + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: NATURAL_GAS + + + + + + training + + + test-org + + natural-gas + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: NUCLEAR + + + + + + training + + + test-org + + nuclear + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: OIL + + + + + + training + + + test-org + + oil + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: OTHER + + + + + + training + + + test-org + + other + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: SOLAR + + + + + + training + + + test-org + + solar + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: UNKNOWN + + + + + + training + + + test-org + + unknown + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + EnergySource: WIND + + + + + + training + + + test-org + + wind + + 1.0 + kWh + + + + 1.0 + kWh + + + + + + + + + + + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/enum_MachineLearningApproach-1.5.json.bin b/tests/_data/snapshots/enum_MachineLearningApproach-1.5.json.bin new file mode 100644 index 00000000..a3c09223 --- /dev/null +++ b/tests/_data/snapshots/enum_MachineLearningApproach-1.5.json.bin @@ -0,0 +1,99 @@ +{ + "components": [ + { + "bom-ref": "dummy-MLA:REINFORCEMENT_LEARNING", + "modelCard": { + "modelParameters": { + "approach": { + "type": "reinforcement-learning" + } + } + }, + "name": "MachineLearningApproach: REINFORCEMENT_LEARNING", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-MLA:SELF_SUPERVISED", + "modelCard": { + "modelParameters": { + "approach": { + "type": "self-supervised" + } + } + }, + "name": "MachineLearningApproach: SELF_SUPERVISED", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-MLA:SEMI_SUPERVISED", + "modelCard": { + "modelParameters": { + "approach": { + "type": "semi-supervised" + } + } + }, + "name": "MachineLearningApproach: SEMI_SUPERVISED", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-MLA:SUPERVISED", + "modelCard": { + "modelParameters": { + "approach": { + "type": "supervised" + } + } + }, + "name": "MachineLearningApproach: SUPERVISED", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-MLA:UNSUPERVISED", + "modelCard": { + "modelParameters": { + "approach": { + "type": "unsupervised" + } + } + }, + "name": "MachineLearningApproach: UNSUPERVISED", + "type": "machine-learning-model" + } + ], + "dependencies": [ + { + "ref": "dummy-MLA:REINFORCEMENT_LEARNING" + }, + { + "ref": "dummy-MLA:SELF_SUPERVISED" + }, + { + "ref": "dummy-MLA:SEMI_SUPERVISED" + }, + { + "ref": "dummy-MLA:SUPERVISED" + }, + { + "ref": "dummy-MLA:UNSUPERVISED" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_MachineLearningApproach-1.5.xml.bin b/tests/_data/snapshots/enum_MachineLearningApproach-1.5.xml.bin new file mode 100644 index 00000000..49196db7 --- /dev/null +++ b/tests/_data/snapshots/enum_MachineLearningApproach-1.5.xml.bin @@ -0,0 +1,69 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + MachineLearningApproach: REINFORCEMENT_LEARNING + + + + reinforcement-learning + + + + + + MachineLearningApproach: SELF_SUPERVISED + + + + self-supervised + + + + + + MachineLearningApproach: SEMI_SUPERVISED + + + + semi-supervised + + + + + + MachineLearningApproach: SUPERVISED + + + + supervised + + + + + + MachineLearningApproach: UNSUPERVISED + + + + unsupervised + + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/enum_MachineLearningApproach-1.6.json.bin b/tests/_data/snapshots/enum_MachineLearningApproach-1.6.json.bin new file mode 100644 index 00000000..74adc496 --- /dev/null +++ b/tests/_data/snapshots/enum_MachineLearningApproach-1.6.json.bin @@ -0,0 +1,99 @@ +{ + "components": [ + { + "bom-ref": "dummy-MLA:REINFORCEMENT_LEARNING", + "modelCard": { + "modelParameters": { + "approach": { + "type": "reinforcement-learning" + } + } + }, + "name": "MachineLearningApproach: REINFORCEMENT_LEARNING", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-MLA:SELF_SUPERVISED", + "modelCard": { + "modelParameters": { + "approach": { + "type": "self-supervised" + } + } + }, + "name": "MachineLearningApproach: SELF_SUPERVISED", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-MLA:SEMI_SUPERVISED", + "modelCard": { + "modelParameters": { + "approach": { + "type": "semi-supervised" + } + } + }, + "name": "MachineLearningApproach: SEMI_SUPERVISED", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-MLA:SUPERVISED", + "modelCard": { + "modelParameters": { + "approach": { + "type": "supervised" + } + } + }, + "name": "MachineLearningApproach: SUPERVISED", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-MLA:UNSUPERVISED", + "modelCard": { + "modelParameters": { + "approach": { + "type": "unsupervised" + } + } + }, + "name": "MachineLearningApproach: UNSUPERVISED", + "type": "machine-learning-model" + } + ], + "dependencies": [ + { + "ref": "dummy-MLA:REINFORCEMENT_LEARNING" + }, + { + "ref": "dummy-MLA:SELF_SUPERVISED" + }, + { + "ref": "dummy-MLA:SEMI_SUPERVISED" + }, + { + "ref": "dummy-MLA:SUPERVISED" + }, + { + "ref": "dummy-MLA:UNSUPERVISED" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_MachineLearningApproach-1.6.xml.bin b/tests/_data/snapshots/enum_MachineLearningApproach-1.6.xml.bin new file mode 100644 index 00000000..e9c848d9 --- /dev/null +++ b/tests/_data/snapshots/enum_MachineLearningApproach-1.6.xml.bin @@ -0,0 +1,69 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + MachineLearningApproach: REINFORCEMENT_LEARNING + + + + reinforcement-learning + + + + + + MachineLearningApproach: SELF_SUPERVISED + + + + self-supervised + + + + + + MachineLearningApproach: SEMI_SUPERVISED + + + + semi-supervised + + + + + + MachineLearningApproach: SUPERVISED + + + + supervised + + + + + + MachineLearningApproach: UNSUPERVISED + + + + unsupervised + + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/enum_MachineLearningApproach-1.7.json.bin b/tests/_data/snapshots/enum_MachineLearningApproach-1.7.json.bin new file mode 100644 index 00000000..450445c5 --- /dev/null +++ b/tests/_data/snapshots/enum_MachineLearningApproach-1.7.json.bin @@ -0,0 +1,99 @@ +{ + "components": [ + { + "bom-ref": "dummy-MLA:REINFORCEMENT_LEARNING", + "modelCard": { + "modelParameters": { + "approach": { + "type": "reinforcement-learning" + } + } + }, + "name": "MachineLearningApproach: REINFORCEMENT_LEARNING", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-MLA:SELF_SUPERVISED", + "modelCard": { + "modelParameters": { + "approach": { + "type": "self-supervised" + } + } + }, + "name": "MachineLearningApproach: SELF_SUPERVISED", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-MLA:SEMI_SUPERVISED", + "modelCard": { + "modelParameters": { + "approach": { + "type": "semi-supervised" + } + } + }, + "name": "MachineLearningApproach: SEMI_SUPERVISED", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-MLA:SUPERVISED", + "modelCard": { + "modelParameters": { + "approach": { + "type": "supervised" + } + } + }, + "name": "MachineLearningApproach: SUPERVISED", + "type": "machine-learning-model" + }, + { + "bom-ref": "dummy-MLA:UNSUPERVISED", + "modelCard": { + "modelParameters": { + "approach": { + "type": "unsupervised" + } + } + }, + "name": "MachineLearningApproach: UNSUPERVISED", + "type": "machine-learning-model" + } + ], + "dependencies": [ + { + "ref": "dummy-MLA:REINFORCEMENT_LEARNING" + }, + { + "ref": "dummy-MLA:SELF_SUPERVISED" + }, + { + "ref": "dummy-MLA:SEMI_SUPERVISED" + }, + { + "ref": "dummy-MLA:SUPERVISED" + }, + { + "ref": "dummy-MLA:UNSUPERVISED" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7" +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_MachineLearningApproach-1.7.xml.bin b/tests/_data/snapshots/enum_MachineLearningApproach-1.7.xml.bin new file mode 100644 index 00000000..f61b02a8 --- /dev/null +++ b/tests/_data/snapshots/enum_MachineLearningApproach-1.7.xml.bin @@ -0,0 +1,69 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + MachineLearningApproach: REINFORCEMENT_LEARNING + + + + reinforcement-learning + + + + + + MachineLearningApproach: SELF_SUPERVISED + + + + self-supervised + + + + + + MachineLearningApproach: SEMI_SUPERVISED + + + + semi-supervised + + + + + + MachineLearningApproach: SUPERVISED + + + + supervised + + + + + + MachineLearningApproach: UNSUPERVISED + + + + unsupervised + + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.5.json.bin b/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.5.json.bin new file mode 100644 index 00000000..1f671fe6 --- /dev/null +++ b/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.5.json.bin @@ -0,0 +1,142 @@ +{ + "components": [ + { + "bom-ref": "ml-card-full", + "modelCard": { + "bom-ref": "mc-full", + "considerations": { + "ethicalConsiderations": [ + { + "mitigationStrategy": "augment data and monitor drift", + "name": "bias" + }, + { + "mitigationStrategy": "anonymize inputs and strip PII", + "name": "privacy" + } + ], + "fairnessAssessments": [ + { + "benefits": "faster responses", + "groupAtRisk": "non-native speakers", + "harms": "potential false positives", + "mitigationStrategy": "rebalance dataset and track metrics" + } + ], + "performanceTradeoffs": [ + "accuracy-over-speed", + "high-memory-footprint" + ], + "technicalLimitations": [ + "limited to 512 tokens", + "may misclassify non-standard language" + ], + "useCases": [ + "content-moderation", + "spam-detection" + ], + "users": [ + "data-scientist", + "ml-engineer" + ] + }, + "modelParameters": { + "approach": { + "type": "supervised" + }, + "architectureFamily": "Transformer", + "inputs": [ + { + "format": "application/json" + }, + { + "format": "text/plain" + } + ], + "modelArchitecture": "tiny-transformer-v1", + "outputs": [ + { + "format": "confidence-score" + }, + { + "format": "label" + } + ], + "task": "text-classification" + }, + "properties": [ + { + "name": "owner", + "value": "ml-team" + }, + { + "name": "release", + "value": "2024-01-01" + } + ], + "quantitativeAnalysis": { + "graphics": { + "collection": [ + { + "image": { + "content": "ZnVsbC1tb2RlbC1jYXJkLWltYWdl", + "contentType": "image/png", + "encoding": "base64" + }, + "name": "pr-curve" + }, + { + "image": { + "content": "ZnVsbC1tb2RlbC1jYXJkLWltYWdl", + "contentType": "image/png", + "encoding": "base64" + }, + "name": "roc-curve" + } + ], + "description": "Performance plots" + }, + "performanceMetrics": [ + { + "slice": "all", + "type": "accuracy", + "value": "0.93" + }, + { + "slice": "en", + "type": "f1", + "value": "0.88" + } + ] + } + }, + "name": "full-model-card-component", + "purl": "pkg:pypi/full-model-card@1.0.0", + "type": "machine-learning-model", + "version": "1.0.0" + } + ], + "dependencies": [ + { + "ref": "ml-card-full" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.5.xml.bin b/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.5.xml.bin new file mode 100644 index 00000000..6436d442 --- /dev/null +++ b/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.5.xml.bin @@ -0,0 +1,113 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + full-model-card-component + 1.0.0 + pkg:pypi/full-model-card@1.0.0 + + + + supervised + + text-classification + Transformer + tiny-transformer-v1 + + + application/json + + + text/plain + + + + + confidence-score + + + label + + + + + + + accuracy + 0.93 + all + + + f1 + 0.88 + en + + + + Performance plots + + + pr-curve + ZnVsbC1tb2RlbC1jYXJkLWltYWdl + + + roc-curve + ZnVsbC1tb2RlbC1jYXJkLWltYWdl + + + + + + + data-scientist + ml-engineer + + + content-moderation + spam-detection + + + limited to 512 tokens + may misclassify non-standard language + + + accuracy-over-speed + high-memory-footprint + + + + bias + augment data and monitor drift + + + privacy + anonymize inputs and strip PII + + + + + non-native speakers + faster responses + potential false positives + rebalance dataset and track metrics + + + + + ml-team + 2024-01-01 + + + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.6.json.bin b/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.6.json.bin new file mode 100644 index 00000000..be2606a3 --- /dev/null +++ b/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.6.json.bin @@ -0,0 +1,221 @@ +{ + "components": [ + { + "bom-ref": "ml-card-full", + "modelCard": { + "bom-ref": "mc-full", + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 42.0 + }, + "co2CostEquivalent": { + "unit": "tCO2eq", + "value": 0.7 + }, + "co2CostOffset": { + "unit": "tCO2eq", + "value": 0.2 + }, + "energyProviders": [ + { + "bom-ref": "mc-energy-provider", + "description": "Primary renewable provider", + "energyProvided": { + "unit": "kWh", + "value": 321.0 + }, + "energySource": "wind", + "externalReferences": [ + { + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "type": "distribution", + "url": "https://cyclonedx.org" + } + ], + "organization": { + "address": { + "country": "GB", + "locality": "Cheshire", + "region": "England", + "streetAddress": "100 Main Street" + }, + "contact": [ + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + }, + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org", + "https://cyclonedx.org/docs" + ] + } + } + ], + "properties": [ + { + "name": "phase", + "value": "pretraining" + } + ] + } + ], + "properties": [ + { + "name": "footprint", + "value": "low" + } + ] + }, + "ethicalConsiderations": [ + { + "mitigationStrategy": "augment data and monitor drift", + "name": "bias" + }, + { + "mitigationStrategy": "anonymize inputs and strip PII", + "name": "privacy" + } + ], + "fairnessAssessments": [ + { + "benefits": "faster responses", + "groupAtRisk": "non-native speakers", + "harms": "potential false positives", + "mitigationStrategy": "rebalance dataset and track metrics" + } + ], + "performanceTradeoffs": [ + "accuracy-over-speed", + "high-memory-footprint" + ], + "technicalLimitations": [ + "limited to 512 tokens", + "may misclassify non-standard language" + ], + "useCases": [ + "content-moderation", + "spam-detection" + ], + "users": [ + "data-scientist", + "ml-engineer" + ] + }, + "modelParameters": { + "approach": { + "type": "supervised" + }, + "architectureFamily": "Transformer", + "inputs": [ + { + "format": "application/json" + }, + { + "format": "text/plain" + } + ], + "modelArchitecture": "tiny-transformer-v1", + "outputs": [ + { + "format": "confidence-score" + }, + { + "format": "label" + } + ], + "task": "text-classification" + }, + "properties": [ + { + "name": "owner", + "value": "ml-team" + }, + { + "name": "release", + "value": "2024-01-01" + } + ], + "quantitativeAnalysis": { + "graphics": { + "collection": [ + { + "image": { + "content": "ZnVsbC1tb2RlbC1jYXJkLWltYWdl", + "contentType": "image/png", + "encoding": "base64" + }, + "name": "pr-curve" + }, + { + "image": { + "content": "ZnVsbC1tb2RlbC1jYXJkLWltYWdl", + "contentType": "image/png", + "encoding": "base64" + }, + "name": "roc-curve" + } + ], + "description": "Performance plots" + }, + "performanceMetrics": [ + { + "slice": "all", + "type": "accuracy", + "value": "0.93" + }, + { + "slice": "en", + "type": "f1", + "value": "0.88" + } + ] + } + }, + "name": "full-model-card-component", + "purl": "pkg:pypi/full-model-card@1.0.0", + "type": "machine-learning-model", + "version": "1.0.0" + } + ], + "dependencies": [ + { + "ref": "ml-card-full" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.6.xml.bin b/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.6.xml.bin new file mode 100644 index 00000000..ec909b28 --- /dev/null +++ b/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.6.xml.bin @@ -0,0 +1,175 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + full-model-card-component + 1.0.0 + pkg:pypi/full-model-card@1.0.0 + + + + supervised + + text-classification + Transformer + tiny-transformer-v1 + + + application/json + + + text/plain + + + + + confidence-score + + + label + + + + + + + accuracy + 0.93 + all + + + f1 + 0.88 + en + + + + Performance plots + + + pr-curve + ZnVsbC1tb2RlbC1jYXJkLWltYWdl + + + roc-curve + ZnVsbC1tb2RlbC1jYXJkLWltYWdl + + + + + + + data-scientist + ml-engineer + + + content-moderation + spam-detection + + + limited to 512 tokens + may misclassify non-standard language + + + accuracy-over-speed + high-memory-footprint + + + + bias + augment data and monitor drift + + + privacy + anonymize inputs and strip PII + + + + + + training + + Primary renewable provider + + CycloneDX +
+ GB + England + Cheshire + 100 Main Street +
+ https://cyclonedx.org + https://cyclonedx.org/docs + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + Paul Horton + paul.horton@owasp.org + +
+ wind + + 321.0 + kWh + + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + +
+ + 42.0 + kWh + + + 0.7 + tCO2eq + + + 0.2 + tCO2eq + + + pretraining + +
+
+ + low + +
+ + + non-native speakers + faster responses + potential false positives + rebalance dataset and track metrics + + +
+ + ml-team + 2024-01-01 + +
+
+
+ + + + + val1 + val2 + +
diff --git a/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.7.json.bin b/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.7.json.bin new file mode 100644 index 00000000..2cdb5f37 --- /dev/null +++ b/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.7.json.bin @@ -0,0 +1,221 @@ +{ + "components": [ + { + "bom-ref": "ml-card-full", + "modelCard": { + "bom-ref": "mc-full", + "considerations": { + "environmentalConsiderations": { + "energyConsumptions": [ + { + "activity": "training", + "activityEnergyCost": { + "unit": "kWh", + "value": 42.0 + }, + "co2CostEquivalent": { + "unit": "tCO2eq", + "value": 0.7 + }, + "co2CostOffset": { + "unit": "tCO2eq", + "value": 0.2 + }, + "energyProviders": [ + { + "bom-ref": "mc-energy-provider", + "description": "Primary renewable provider", + "energyProvided": { + "unit": "kWh", + "value": 321.0 + }, + "energySource": "wind", + "externalReferences": [ + { + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "type": "distribution", + "url": "https://cyclonedx.org" + } + ], + "organization": { + "address": { + "country": "GB", + "locality": "Cheshire", + "region": "England", + "streetAddress": "100 Main Street" + }, + "contact": [ + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + }, + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org", + "https://cyclonedx.org/docs" + ] + } + } + ], + "properties": [ + { + "name": "phase", + "value": "pretraining" + } + ] + } + ], + "properties": [ + { + "name": "footprint", + "value": "low" + } + ] + }, + "ethicalConsiderations": [ + { + "mitigationStrategy": "augment data and monitor drift", + "name": "bias" + }, + { + "mitigationStrategy": "anonymize inputs and strip PII", + "name": "privacy" + } + ], + "fairnessAssessments": [ + { + "benefits": "faster responses", + "groupAtRisk": "non-native speakers", + "harms": "potential false positives", + "mitigationStrategy": "rebalance dataset and track metrics" + } + ], + "performanceTradeoffs": [ + "accuracy-over-speed", + "high-memory-footprint" + ], + "technicalLimitations": [ + "limited to 512 tokens", + "may misclassify non-standard language" + ], + "useCases": [ + "content-moderation", + "spam-detection" + ], + "users": [ + "data-scientist", + "ml-engineer" + ] + }, + "modelParameters": { + "approach": { + "type": "supervised" + }, + "architectureFamily": "Transformer", + "inputs": [ + { + "format": "application/json" + }, + { + "format": "text/plain" + } + ], + "modelArchitecture": "tiny-transformer-v1", + "outputs": [ + { + "format": "confidence-score" + }, + { + "format": "label" + } + ], + "task": "text-classification" + }, + "properties": [ + { + "name": "owner", + "value": "ml-team" + }, + { + "name": "release", + "value": "2024-01-01" + } + ], + "quantitativeAnalysis": { + "graphics": { + "collection": [ + { + "image": { + "content": "ZnVsbC1tb2RlbC1jYXJkLWltYWdl", + "contentType": "image/png", + "encoding": "base64" + }, + "name": "pr-curve" + }, + { + "image": { + "content": "ZnVsbC1tb2RlbC1jYXJkLWltYWdl", + "contentType": "image/png", + "encoding": "base64" + }, + "name": "roc-curve" + } + ], + "description": "Performance plots" + }, + "performanceMetrics": [ + { + "slice": "all", + "type": "accuracy", + "value": "0.93" + }, + { + "slice": "en", + "type": "f1", + "value": "0.88" + } + ] + } + }, + "name": "full-model-card-component", + "purl": "pkg:pypi/full-model-card@1.0.0", + "type": "machine-learning-model", + "version": "1.0.0" + } + ], + "dependencies": [ + { + "ref": "ml-card-full" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.7.xml.bin b/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.7.xml.bin new file mode 100644 index 00000000..88d2ac80 --- /dev/null +++ b/tests/_data/snapshots/get_bom_v1_5_with_full_model_card-1.7.xml.bin @@ -0,0 +1,175 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + full-model-card-component + 1.0.0 + pkg:pypi/full-model-card@1.0.0 + + + + supervised + + text-classification + Transformer + tiny-transformer-v1 + + + application/json + + + text/plain + + + + + confidence-score + + + label + + + + + + + accuracy + 0.93 + all + + + f1 + 0.88 + en + + + + Performance plots + + + pr-curve + ZnVsbC1tb2RlbC1jYXJkLWltYWdl + + + roc-curve + ZnVsbC1tb2RlbC1jYXJkLWltYWdl + + + + + + + data-scientist + ml-engineer + + + content-moderation + spam-detection + + + limited to 512 tokens + may misclassify non-standard language + + + accuracy-over-speed + high-memory-footprint + + + + bias + augment data and monitor drift + + + privacy + anonymize inputs and strip PII + + + + + + training + + Primary renewable provider + + CycloneDX +
+ GB + England + Cheshire + 100 Main Street +
+ https://cyclonedx.org + https://cyclonedx.org/docs + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + Paul Horton + paul.horton@owasp.org + +
+ wind + + 321.0 + kWh + + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + +
+ + 42.0 + kWh + + + 0.7 + tCO2eq + + + 0.2 + tCO2eq + + + pretraining + +
+
+ + low + +
+ + + non-native speakers + faster responses + potential false positives + rebalance dataset and track metrics + + +
+ + ml-team + 2024-01-01 + +
+
+
+ + + + + val1 + val2 + +
diff --git a/tests/test_enums.py b/tests/test_enums.py index 88ac8e71..94d0a99c 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -36,6 +36,7 @@ from cyclonedx.model.bom import Bom, BomMetaData, DistributionConstraints, TlpClassification from cyclonedx.model.component import Component, Patch, Pedigree from cyclonedx.model.component_evidence import ComponentEvidence, Identity as CEIdentity, Method as CEMethod +from cyclonedx.model.contact import OrganizationalEntity from cyclonedx.model.crypto import ( AlgorithmProperties, CryptoProperties, @@ -45,6 +46,17 @@ from cyclonedx.model.issue import IssueType from cyclonedx.model.license import DisjunctiveLicense from cyclonedx.model.lifecycle import LifecyclePhase, PredefinedLifecycle +from cyclonedx.model.model_card import ( + Approach, + Co2Measure, + Considerations, + EnergyConsumption, + EnergyMeasure, + EnergyProvider, + EnvironmentalConsiderations, + ModelCard, + ModelParameters, +) from cyclonedx.model.service import DataClassification, Service from cyclonedx.model.vulnerability import ( BomTarget, @@ -106,6 +118,13 @@ RelatedCryptoMaterialState, RelatedCryptoMaterialType, ) +from cyclonedx.model.model_card import ( # isort:skip + Co2MeasureUnit, + EnergyActivity, + EnergyMeasureUnit, + EnergySource, + MachineLearningApproach, +) # endregion SUT @@ -977,6 +996,199 @@ def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, super()._test_cases_render(bom, of, sv) +@ddt +class TestEnumMachineLearningApproach(_EnumTestCase): + + @idata(set(chain( + dp_cases_from_xml_schemas(f"./{SCHEMA_NS}simpleType[@name='machineLearningApproachType']"), + dp_cases_from_json_schemas('definitions', 'modelCard', 'properties', 'modelParameters', + 'properties', 'approach', 'properties', 'type'), + ))) + def test_knows_value(self, value: str) -> None: + super()._test_knows_value(MachineLearningApproach, value) + + @named_data(*(d for d in NAMED_OF_SV if d[2] >= SchemaVersion.V1_5)) + def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + bom = _make_bom( + components=[ + Component( + name=f'MachineLearningApproach: {mla.name}', bom_ref=f'dummy-MLA:{mla.name}', + type=ComponentType.MACHINE_LEARNING_MODEL, + model_card=ModelCard( + model_parameters=ModelParameters( + approach=Approach(type=mla) + ) + ) + ) for mla in MachineLearningApproach + ]) + super()._test_cases_render(bom, of, sv) + + +@ddt +class TestEnumEnergyActivity(_EnumTestCase): + + @idata(set(chain( + dp_cases_from_xml_schemas( + f"./{SCHEMA_NS}complexType[@name='energyConsumptionType']/{SCHEMA_NS}sequence" + f"/{SCHEMA_NS}element[@name='activity']/{SCHEMA_NS}simpleType"), + dp_cases_from_json_schemas('definitions', 'energyConsumption', 'properties', 'activity'), + ))) + def test_knows_value(self, value: str) -> None: + super()._test_knows_value(EnergyActivity, value) + + @named_data(*(d for d in NAMED_OF_SV if d[2] >= SchemaVersion.V1_6)) + def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + bom = _make_bom( + components=[ + Component( + name=f'EnergyActivity: {ea.name}', bom_ref=f'dummy-EA:{ea.name}', + type=ComponentType.MACHINE_LEARNING_MODEL, + model_card=ModelCard( + considerations=Considerations( + environmental_considerations=EnvironmentalConsiderations( + energy_consumptions=[ + EnergyConsumption( + activity=ea, + energy_providers=[EnergyProvider( + organization=OrganizationalEntity(name='test-org'), + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=1.0), + )], + activity_energy_cost=EnergyMeasure(value=1.0), + ) + ] + ) + ) + ) + ) for ea in EnergyActivity + ]) + super()._test_cases_render(bom, of, sv) + + +@ddt +class TestEnumEnergySource(_EnumTestCase): + + @idata(set(chain( + dp_cases_from_xml_schemas( + f"./{SCHEMA_NS}complexType[@name='energyProviderType']/{SCHEMA_NS}sequence" + f"/{SCHEMA_NS}element[@name='energySource']/{SCHEMA_NS}simpleType"), + dp_cases_from_json_schemas('definitions', 'energyProvider', 'properties', 'energySource'), + ))) + def test_knows_value(self, value: str) -> None: + super()._test_knows_value(EnergySource, value) + + @named_data(*(d for d in NAMED_OF_SV if d[2] >= SchemaVersion.V1_6)) + def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + bom = _make_bom( + components=[ + Component( + name=f'EnergySource: {es.name}', bom_ref=f'dummy-ES:{es.name}', + type=ComponentType.MACHINE_LEARNING_MODEL, + model_card=ModelCard( + considerations=Considerations( + environmental_considerations=EnvironmentalConsiderations( + energy_consumptions=[ + EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[EnergyProvider( + organization=OrganizationalEntity(name='test-org'), + energy_source=es, + energy_provided=EnergyMeasure(value=1.0), + )], + activity_energy_cost=EnergyMeasure(value=1.0), + ) + ] + ) + ) + ) + ) for es in EnergySource + ]) + super()._test_cases_render(bom, of, sv) + + +@ddt +class TestEnumEnergyMeasureUnit(_EnumTestCase): + + @idata(set(chain( + dp_cases_from_xml_schemas( + f"./{SCHEMA_NS}complexType[@name='energyMeasureType']/{SCHEMA_NS}sequence" + f"/{SCHEMA_NS}element[@name='unit']/{SCHEMA_NS}simpleType"), + dp_cases_from_json_schemas('definitions', 'energyMeasure', 'properties', 'unit'), + ))) + def test_knows_value(self, value: str) -> None: + super()._test_knows_value(EnergyMeasureUnit, value) + + @named_data(*(d for d in NAMED_OF_SV if d[2] >= SchemaVersion.V1_6)) + def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + bom = _make_bom( + components=[ + Component( + name=f'EnergyMeasureUnit: {emu.name}', bom_ref=f'dummy-EMU:{emu.name}', + type=ComponentType.MACHINE_LEARNING_MODEL, + model_card=ModelCard( + considerations=Considerations( + environmental_considerations=EnvironmentalConsiderations( + energy_consumptions=[ + EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[EnergyProvider( + organization=OrganizationalEntity(name='test-org'), + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=1.0, unit=emu), + )], + activity_energy_cost=EnergyMeasure(value=1.0, unit=emu), + ) + ] + ) + ) + ) + ) for emu in EnergyMeasureUnit + ]) + super()._test_cases_render(bom, of, sv) + + +@ddt +class TestEnumCo2MeasureUnit(_EnumTestCase): + + @idata(set(chain( + dp_cases_from_xml_schemas( + f"./{SCHEMA_NS}complexType[@name='co2MeasureType']/{SCHEMA_NS}sequence" + f"/{SCHEMA_NS}element[@name='unit']/{SCHEMA_NS}simpleType"), + dp_cases_from_json_schemas('definitions', 'co2Measure', 'properties', 'unit'), + ))) + def test_knows_value(self, value: str) -> None: + super()._test_knows_value(Co2MeasureUnit, value) + + @named_data(*(d for d in NAMED_OF_SV if d[2] >= SchemaVersion.V1_6)) + def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + bom = _make_bom( + components=[ + Component( + name=f'Co2MeasureUnit: {cmu.name}', bom_ref=f'dummy-CMU:{cmu.name}', + type=ComponentType.MACHINE_LEARNING_MODEL, + model_card=ModelCard( + considerations=Considerations( + environmental_considerations=EnvironmentalConsiderations( + energy_consumptions=[ + EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[EnergyProvider( + organization=OrganizationalEntity(name='test-org'), + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=1.0), + )], + activity_energy_cost=EnergyMeasure(value=1.0), + co2_cost_equivalent=Co2Measure(value=1.0, unit=cmu), + ) + ] + ) + ) + ) + ) for cmu in Co2MeasureUnit + ]) + super()._test_cases_render(bom, of, sv) + + # add new test cases above this line diff --git a/tests/test_model_model_card.py b/tests/test_model_model_card.py new file mode 100644 index 00000000..6ee2c969 --- /dev/null +++ b/tests/test_model_model_card.py @@ -0,0 +1,1235 @@ +# This file is part of CycloneDX Python Library +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +from unittest import TestCase + +from cyclonedx.model import ExternalReference, ExternalReferenceType, Property +from cyclonedx.model.contact import OrganizationalEntity +from cyclonedx.model.model_card import ( + Approach, + Co2Measure, + Co2MeasureUnit, + ConfidenceInterval, + Considerations, + EnergyActivity, + EnergyConsumption, + EnergyMeasure, + EnergyMeasureUnit, + EnergyProvider, + EnergySource, + EnvironmentalConsiderations, + EthicalConsideration, + FairnessAssessment, + Graphic, + GraphicsCollection, + InputOutputMLParameters, + MachineLearningApproach, + ModelCard, + ModelParameters, + PerformanceMetric, + QuantitativeAnalysis, +) +from tests import reorder + + +class TestCo2MeasureUnit(TestCase): + + def test_enum_value(self) -> None: + self.assertEqual(Co2MeasureUnit.TCO2EQ.value, 'tCO2eq') + + def test_enum_comparison(self) -> None: + self.assertEqual(Co2MeasureUnit.TCO2EQ, Co2MeasureUnit('tCO2eq')) + + +class TestEnergyMeasureUnit(TestCase): + + def test_enum_value(self) -> None: + self.assertEqual(EnergyMeasureUnit.KWH.value, 'kWh') + + def test_enum_comparison(self) -> None: + self.assertEqual(EnergyMeasureUnit.KWH, EnergyMeasureUnit('kWh')) + + +class TestMachineLearningApproach(TestCase): + + def test_all_values_exist(self) -> None: + self.assertEqual( + { + MachineLearningApproach.SUPERVISED.value, + MachineLearningApproach.UNSUPERVISED.value, + MachineLearningApproach.REINFORCEMENT_LEARNING.value, + MachineLearningApproach.SEMI_SUPERVISED.value, + MachineLearningApproach.SELF_SUPERVISED.value, + }, + { + 'supervised', + 'unsupervised', + 'reinforcement-learning', + 'semi-supervised', + 'self-supervised', + }, + ) + + +class TestApproach(TestCase): + def test_create(self) -> None: + approach_sup = Approach(type=MachineLearningApproach.SUPERVISED) + approach_unsup = Approach(type=MachineLearningApproach.UNSUPERVISED) + approach_reinf = Approach(type=MachineLearningApproach.REINFORCEMENT_LEARNING) + approach_semi = Approach(type=MachineLearningApproach.SEMI_SUPERVISED) + approach_self = Approach(type=MachineLearningApproach.SELF_SUPERVISED) + self.assertIs(MachineLearningApproach.SUPERVISED, approach_sup.type) + self.assertIs(MachineLearningApproach.UNSUPERVISED, approach_unsup.type) + self.assertIs(MachineLearningApproach.REINFORCEMENT_LEARNING, approach_reinf.type) + self.assertIs(MachineLearningApproach.SEMI_SUPERVISED, approach_semi.type) + self.assertIs(MachineLearningApproach.SELF_SUPERVISED, approach_self.type) + + def test_update(self) -> None: + approach = Approach(type=MachineLearningApproach.SUPERVISED) + self.assertIs(MachineLearningApproach.SUPERVISED, approach.type) + approach.type = MachineLearningApproach.UNSUPERVISED + self.assertIs(MachineLearningApproach.UNSUPERVISED, approach.type) + + def test_sort(self) -> None: + expected_order = [4, 3, 2, 1, 0] + approaches = [ + Approach(type=MachineLearningApproach.UNSUPERVISED), + Approach(type=MachineLearningApproach.SUPERVISED), + Approach(type=MachineLearningApproach.SEMI_SUPERVISED), + Approach(type=MachineLearningApproach.SELF_SUPERVISED), + Approach(type=MachineLearningApproach.REINFORCEMENT_LEARNING), + ] + expected_approaches = reorder(approaches, expected_order) + sorted_approaches = sorted(approaches) + self.assertListEqual(sorted_approaches, expected_approaches) + + def test_no_params(self) -> None: + approach = Approach() + self.assertIsNone(approach.type) + + def test_same(self) -> None: + approach_1 = Approach(type=MachineLearningApproach.SUPERVISED) + approach_2 = Approach(type=MachineLearningApproach.SUPERVISED) + + self.assertNotEqual(id(approach_1), id(approach_2)) + self.assertEqual(hash(approach_1), hash(approach_2)) + self.assertTrue(approach_1 == approach_2) + + def test_not_same(self) -> None: + approach_1 = Approach(type=MachineLearningApproach.SUPERVISED) + approach_2 = Approach(type=MachineLearningApproach.UNSUPERVISED) + + self.assertNotEqual(hash(approach_1), hash(approach_2)) + self.assertFalse(approach_1 == approach_2) + + def test_compare_same_type(self) -> None: + approach_1 = Approach(type=MachineLearningApproach.SUPERVISED) + approach_2 = Approach(type=MachineLearningApproach.SUPERVISED) + + self.assertFalse(approach_1 < approach_2) + self.assertTrue(approach_1 <= approach_2) + self.assertTrue(approach_1 >= approach_2) + + def test_repr(self) -> None: + approach = Approach(type=MachineLearningApproach.SUPERVISED) + + # 3.10 returns the value, 3.11+ returns the enum member name; accept both + self.assertIn( + repr(approach), + { + '', + '', + }, + ) + + +class TestInputOutputMLParameters(TestCase): + + def test_constructor_and_property(self) -> None: + obj = InputOutputMLParameters(format='JSON') + + self.assertEqual(obj.format, 'JSON') + + def test_setter(self) -> None: + obj = InputOutputMLParameters(format='JSON') + + obj.format = 'CSV' + + self.assertEqual(obj.format, 'CSV') + + def test_equality(self) -> None: + self.assertEqual( + InputOutputMLParameters(format='JSON'), + InputOutputMLParameters(format='JSON'), + ) + + def test_inequality(self) -> None: + self.assertNotEqual( + InputOutputMLParameters(format='JSON'), + InputOutputMLParameters(format='CSV'), + ) + + def test_comparison(self) -> None: + first = InputOutputMLParameters(format='CSV') + second = InputOutputMLParameters(format='JSON') + + self.assertTrue(first < second) + self.assertTrue(first <= second) + self.assertTrue(second >= first) + + def test_hash(self) -> None: + self.assertEqual( + hash(InputOutputMLParameters(format='JSON')), + hash(InputOutputMLParameters(format='JSON')), + ) + + def test_repr(self) -> None: + self.assertEqual( + repr(InputOutputMLParameters(format='JSON')), + "", + ) + + +class TestModelParameters(TestCase): + + def test_defaults(self) -> None: + obj = ModelParameters() + + self.assertIsNone(obj.approach) + self.assertIsNone(obj.task) + self.assertEqual(len(obj.inputs), 0) + self.assertEqual(len(obj.outputs), 0) + + def test_constructor(self) -> None: + input_param = InputOutputMLParameters(format='JSON') + output_param = InputOutputMLParameters(format='TEXT') + + obj = ModelParameters( + task='classification', + architecture_family='transformer', + model_architecture='bert', + inputs=[input_param], + outputs=[output_param], + ) + + self.assertEqual(obj.task, 'classification') + self.assertEqual(obj.architecture_family, 'transformer') + self.assertEqual(obj.model_architecture, 'bert') + self.assertIn(input_param, obj.inputs) + self.assertIn(output_param, obj.outputs) + + def test_property_setters(self) -> None: + obj = ModelParameters() + + obj.task = 'generation' + obj.architecture_family = 'cnn' + obj.model_architecture = 'resnet' + + self.assertEqual(obj.task, 'generation') + self.assertEqual(obj.architecture_family, 'cnn') + self.assertEqual(obj.model_architecture, 'resnet') + + def test_dataset_not_supported(self) -> None: + with self.assertRaises(NotImplementedError): + ModelParameters(datasets=['dataset']) + + def test_sorted_input_collection(self) -> None: + first = InputOutputMLParameters(format='A') + second = InputOutputMLParameters(format='B') + + obj = ModelParameters(inputs=[second, first]) + + self.assertEqual( + list(obj.inputs), + [first, second], + ) + + def test_equality(self) -> None: + self.assertEqual( + ModelParameters(task='test'), + ModelParameters(task='test'), + ) + + def test_inequality(self) -> None: + self.assertNotEqual( + ModelParameters(task='a'), + ModelParameters(task='b'), + ) + + def test_comparison(self) -> None: + first = ModelParameters(task='a') + second = ModelParameters(task='b') + + self.assertTrue(first < second) + self.assertTrue(first <= second) + self.assertTrue(second >= first) + + def test_hash(self) -> None: + self.assertEqual( + hash(ModelParameters(task='test')), + hash(ModelParameters(task='test')), + ) + + def test_repr(self) -> None: + self.assertEqual( + repr(ModelParameters(task='classification')), + "", + ) + + +class TestConfidenceInterval(TestCase): + + def test_defaults(self) -> None: + obj = ConfidenceInterval() + + self.assertIsNone(obj.lower_bound) + self.assertIsNone(obj.upper_bound) + + def test_constructor(self) -> None: + obj = ConfidenceInterval( + lower_bound='0.8', + upper_bound='0.9', + ) + + self.assertEqual(obj.lower_bound, '0.8') + self.assertEqual(obj.upper_bound, '0.9') + + def test_setters(self) -> None: + obj = ConfidenceInterval() + + obj.lower_bound = '0.1' + obj.upper_bound = '0.2' + + self.assertEqual(obj.lower_bound, '0.1') + self.assertEqual(obj.upper_bound, '0.2') + + def test_equality(self) -> None: + self.assertEqual( + ConfidenceInterval( + lower_bound='0.1', + upper_bound='0.2', + ), + ConfidenceInterval( + lower_bound='0.1', + upper_bound='0.2', + ), + ) + + def test_comparison(self) -> None: + self.assertTrue( + ConfidenceInterval( + lower_bound='0.1', + upper_bound='0.2', + ) + < ConfidenceInterval( + lower_bound='0.2', + upper_bound='0.3', + ) + ) + + def test_hash(self) -> None: + self.assertEqual( + hash(ConfidenceInterval( + lower_bound='0.1', + upper_bound='0.2', + )), + hash(ConfidenceInterval( + lower_bound='0.1', + upper_bound='0.2', + )), + ) + + def test_repr(self) -> None: + self.assertEqual( + repr(ConfidenceInterval( + lower_bound='0.1', + upper_bound='0.2', + )), + '', + ) + + +class TestPerformanceMetric(TestCase): + + def test_constructor(self) -> None: + interval = ConfidenceInterval( + lower_bound='0.8', + upper_bound='0.9', + ) + + obj = PerformanceMetric( + type='accuracy', + value='0.95', + slice_='test', + confidence_interval=interval, + ) + + self.assertEqual(obj.type, 'accuracy') + self.assertEqual(obj.value, '0.95') + self.assertEqual(obj.slice_, 'test') + self.assertEqual(obj.confidence_interval, interval) + + def test_setters(self) -> None: + obj = PerformanceMetric() + + obj.type = 'precision' + obj.value = '0.8' + + self.assertEqual(obj.type, 'precision') + self.assertEqual(obj.value, '0.8') + + def test_equality(self) -> None: + self.assertEqual( + PerformanceMetric(type='accuracy', value='1'), + PerformanceMetric(type='accuracy', value='1'), + ) + + def test_comparison(self) -> None: + self.assertTrue( + PerformanceMetric(type='a') + < PerformanceMetric(type='b') + ) + + def test_hash(self) -> None: + self.assertEqual( + hash(PerformanceMetric(type='accuracy')), + hash(PerformanceMetric(type='accuracy')), + ) + + def test_repr(self) -> None: + self.assertEqual( + repr(PerformanceMetric(type='accuracy', value='1')), + "", + ) + + +class TestGraphic(TestCase): + + def test_constructor(self) -> None: + obj = Graphic(name='plot') + + self.assertEqual(obj.name, 'plot') + self.assertIsNone(obj.image) + + def test_setters(self) -> None: + obj = Graphic() + + obj.name = 'chart' + + self.assertEqual(obj.name, 'chart') + + def test_equality(self) -> None: + self.assertEqual( + Graphic(name='a'), + Graphic(name='a'), + ) + + def test_comparison(self) -> None: + self.assertTrue( + Graphic(name='a') + < Graphic(name='b') + ) + + def test_hash(self) -> None: + self.assertEqual( + hash(Graphic(name='a')), + hash(Graphic(name='a')), + ) + + def test_repr(self) -> None: + self.assertEqual( + repr(Graphic(name='chart')), + "", + ) + + +class TestGraphicsCollection(TestCase): + + def test_defaults(self) -> None: + obj = GraphicsCollection() + + self.assertEqual(len(obj.collection), 0) + + def test_constructor(self) -> None: + graphics = [ + Graphic(name='b'), + Graphic(name='a'), + ] + + obj = GraphicsCollection( + description='images', + collection=graphics, + ) + + self.assertEqual(obj.description, 'images') + self.assertEqual( + list(obj.collection), + sorted(graphics), + ) + + def test_sorted_collection(self) -> None: + obj = GraphicsCollection( + collection=[ + Graphic(name='b'), + Graphic(name='a'), + ] + ) + + self.assertEqual( + [g.name for g in obj.collection], + ['a', 'b'], + ) + + def test_equality(self) -> None: + self.assertEqual( + GraphicsCollection(description='x'), + GraphicsCollection(description='x'), + ) + + def test_comparison(self) -> None: + self.assertTrue( + GraphicsCollection(description='a') + < GraphicsCollection(description='b') + ) + + def test_hash(self) -> None: + self.assertEqual( + hash(GraphicsCollection(description='x')), + hash(GraphicsCollection(description='x')), + ) + + def test_repr(self) -> None: + self.assertEqual( + repr(GraphicsCollection()), + '', + ) + + +class TestQuantitativeAnalysis(TestCase): + + def test_defaults(self) -> None: + obj = QuantitativeAnalysis() + + self.assertEqual(len(obj.performance_metrics), 0) + self.assertIsNone(obj.graphics) + + def test_constructor(self) -> None: + metric = PerformanceMetric( + type='accuracy', + value='0.9', + ) + graphics = GraphicsCollection(description='plots') + + obj = QuantitativeAnalysis( + performance_metrics=[metric], + graphics=graphics, + ) + + self.assertIn(metric, obj.performance_metrics) + self.assertEqual(obj.graphics, graphics) + + def test_sorted_metrics(self) -> None: + first = PerformanceMetric(type='a') + second = PerformanceMetric(type='b') + + obj = QuantitativeAnalysis( + performance_metrics=[second, first], + ) + + self.assertEqual( + list(obj.performance_metrics), + [first, second], + ) + + def test_setter(self) -> None: + obj = QuantitativeAnalysis() + + obj.graphics = GraphicsCollection() + + self.assertIsNotNone(obj.graphics) + + def test_equality(self) -> None: + self.assertEqual( + QuantitativeAnalysis(), + QuantitativeAnalysis(), + ) + + def test_comparison(self) -> None: + first = QuantitativeAnalysis( + performance_metrics=[PerformanceMetric(type='a')] + ) + second = QuantitativeAnalysis( + performance_metrics=[PerformanceMetric(type='b')] + ) + + self.assertTrue(first < second) + + def test_hash(self) -> None: + self.assertEqual( + hash(QuantitativeAnalysis()), + hash(QuantitativeAnalysis()), + ) + + def test_repr(self) -> None: + self.assertEqual( + repr(QuantitativeAnalysis()), + '', + ) + + +class TestEthicalConsideration(TestCase): + + def test_defaults(self) -> None: + obj = EthicalConsideration() + + self.assertIsNone(obj.name) + self.assertIsNone(obj.mitigation_strategy) + + def test_constructor(self) -> None: + obj = EthicalConsideration( + name='privacy', + mitigation_strategy='anonymization', + ) + + self.assertEqual(obj.name, 'privacy') + self.assertEqual(obj.mitigation_strategy, 'anonymization') + + def test_setters(self) -> None: + obj = EthicalConsideration() + + obj.name = 'fairness' + obj.mitigation_strategy = 'bias monitoring' + + self.assertEqual(obj.name, 'fairness') + self.assertEqual(obj.mitigation_strategy, 'bias monitoring') + + def test_equality(self) -> None: + self.assertEqual( + EthicalConsideration( + name='a', + mitigation_strategy='b', + ), + EthicalConsideration( + name='a', + mitigation_strategy='b', + ), + ) + + def test_inequality(self) -> None: + self.assertNotEqual( + EthicalConsideration( + name='a', + mitigation_strategy='b', + ), + EthicalConsideration( + name='a', + mitigation_strategy='c', + ), + ) + + def test_comparison(self) -> None: + self.assertTrue( + EthicalConsideration(name='a') + < EthicalConsideration(name='b') + ) + + def test_hash(self) -> None: + self.assertEqual( + hash(EthicalConsideration(name='a')), + hash(EthicalConsideration(name='a')), + ) + + def test_repr(self) -> None: + self.assertEqual( + repr(EthicalConsideration(name='privacy')), + "", + ) + + +class TestFairnessAssessment(TestCase): + + def test_defaults(self) -> None: + obj = FairnessAssessment() + + self.assertIsNone(obj.group_at_risk) + self.assertIsNone(obj.benefits) + self.assertIsNone(obj.harms) + self.assertIsNone(obj.mitigation_strategy) + + def test_constructor(self) -> None: + obj = FairnessAssessment( + group_at_risk='users', + benefits='equal outcomes', + harms='bias', + mitigation_strategy='retraining', + ) + + self.assertEqual(obj.group_at_risk, 'users') + self.assertEqual(obj.benefits, 'equal outcomes') + self.assertEqual(obj.harms, 'bias') + self.assertEqual(obj.mitigation_strategy, 'retraining') + + def test_setters(self) -> None: + obj = FairnessAssessment() + + obj.group_at_risk = 'group' + + self.assertEqual(obj.group_at_risk, 'group') + + def test_equality(self) -> None: + self.assertEqual( + FairnessAssessment(group_at_risk='a'), + FairnessAssessment(group_at_risk='a'), + ) + + def test_comparison(self) -> None: + self.assertTrue( + FairnessAssessment(group_at_risk='a') + < FairnessAssessment(group_at_risk='b') + ) + + def test_hash(self) -> None: + self.assertEqual( + hash(FairnessAssessment(group_at_risk='a')), + hash(FairnessAssessment(group_at_risk='a')), + ) + + def test_repr(self) -> None: + self.assertEqual( + repr(FairnessAssessment(group_at_risk='users')), + "", + ) + + +class TestEnvironmentalConsiderations(TestCase): + + def test_defaults(self) -> None: + obj = EnvironmentalConsiderations() + + self.assertEqual(len(obj.energy_consumptions), 0) + self.assertEqual(len(obj.properties), 0) + + def test_constructor(self) -> None: + obj = EnvironmentalConsiderations( + energy_consumptions=[], + properties=[], + ) + + self.assertEqual(len(obj.energy_consumptions), 0) + self.assertEqual(len(obj.properties), 0) + + def test_sorted_properties(self) -> None: + prop_a = Property(name='a', value='1') + prop_b = Property(name='b', value='2') + + obj = EnvironmentalConsiderations( + properties=[prop_b, prop_a], + ) + + self.assertEqual( + list(obj.properties), + [prop_a, prop_b], + ) + + def test_energy_consumptions(self) -> None: + consumption = EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[], + activity_energy_cost=EnergyMeasure(value=10), + ) + + obj = EnvironmentalConsiderations( + energy_consumptions=[consumption], + ) + + self.assertIn(consumption, obj.energy_consumptions) + + def test_equality(self) -> None: + self.assertEqual( + EnvironmentalConsiderations(), + EnvironmentalConsiderations(), + ) + + def test_comparison(self) -> None: + self.assertTrue( + EnvironmentalConsiderations() + <= EnvironmentalConsiderations() + ) + + def test_hash(self) -> None: + self.assertEqual( + hash(EnvironmentalConsiderations()), + hash(EnvironmentalConsiderations()), + ) + + def test_repr(self) -> None: + self.assertEqual( + repr(EnvironmentalConsiderations()), + '', + ) + + +class TestEnergyActivity(TestCase): + + def test_values_exist(self) -> None: + self.assertEqual( + EnergyActivity.TRAINING.value, + 'training', + ) + + self.assertEqual( + EnergyActivity.INFERENCE.value, + 'inference', + ) + + +class TestEnergyMeasure(TestCase): + + def test_defaults(self) -> None: + obj = EnergyMeasure(value=10.5) + + self.assertEqual(obj.value, 10.5) + self.assertEqual(obj.unit, EnergyMeasureUnit.KWH) + + def test_constructor(self) -> None: + obj = EnergyMeasure( + value=20, + unit=EnergyMeasureUnit.KWH, + ) + + self.assertEqual(obj.value, 20) + + def test_setters(self) -> None: + obj = EnergyMeasure(value=1) + + obj.value = 5 + + self.assertEqual(obj.value, 5) + + def test_equality(self) -> None: + self.assertEqual( + EnergyMeasure(value=1), + EnergyMeasure(value=1), + ) + + def test_comparison(self) -> None: + self.assertTrue( + EnergyMeasure(value=1) + < EnergyMeasure(value=2) + ) + + def test_hash(self) -> None: + self.assertEqual( + hash(EnergyMeasure(value=1)), + hash(EnergyMeasure(value=1)), + ) + + def test_repr(self) -> None: + self.assertIn( + 'EnergyMeasure', + repr(EnergyMeasure(value=1)), + ) + + +class TestCo2Measure(TestCase): + + def test_defaults(self) -> None: + obj = Co2Measure(value=1.5) + + self.assertEqual(obj.value, 1.5) + self.assertEqual(obj.unit, Co2MeasureUnit.TCO2EQ) + + def test_constructor(self) -> None: + obj = Co2Measure( + value=10, + unit=Co2MeasureUnit.TCO2EQ, + ) + + self.assertEqual(obj.value, 10) + + def test_setters(self) -> None: + obj = Co2Measure(value=1) + + obj.value = 2 + + self.assertEqual(obj.value, 2) + + def test_equality(self) -> None: + self.assertEqual( + Co2Measure(value=1), + Co2Measure(value=1), + ) + + def test_comparison(self) -> None: + self.assertTrue( + Co2Measure(value=1) + < Co2Measure(value=2) + ) + + def test_hash(self) -> None: + self.assertEqual( + hash(Co2Measure(value=1)), + hash(Co2Measure(value=1)), + ) + + def test_repr(self) -> None: + self.assertIn( + 'Co2Measure', + repr(Co2Measure(value=1)), + ) + + +class TestEnergySource(TestCase): + + def test_values_exist(self) -> None: + self.assertEqual(EnergySource.SOLAR.value, 'solar') + self.assertEqual(EnergySource.WIND.value, 'wind') + self.assertEqual(EnergySource.UNKNOWN.value, 'unknown') + + +class TestEnergyProvider(TestCase): + + def _organization(self) -> OrganizationalEntity: + return OrganizationalEntity(name='provider') + + def test_constructor(self) -> None: + obj = EnergyProvider( + organization=self._organization(), + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=100), + ) + + self.assertEqual(obj.energy_source, EnergySource.SOLAR) + self.assertEqual(obj.energy_provided, EnergyMeasure(value=100)) + + def test_description(self) -> None: + obj = EnergyProvider( + organization=self._organization(), + energy_source=EnergySource.WIND, + energy_provided=EnergyMeasure(value=10), + description='green energy', + ) + + self.assertEqual(obj.description, 'green energy') + + def test_bom_ref(self) -> None: + obj = EnergyProvider( + organization=self._organization(), + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=1), + bom_ref='provider-ref', + ) + + self.assertEqual(obj.bom_ref.value, 'provider-ref') + + def test_external_references(self) -> None: + reference = ExternalReference( + type=ExternalReferenceType.WEBSITE, + url='https://example.com', + ) + + obj = EnergyProvider( + organization=self._organization(), + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=1), + external_references=[reference], + ) + + self.assertIn(reference, obj.external_references) + + def test_equality(self) -> None: + organization = self._organization() + + self.assertEqual( + EnergyProvider( + organization=organization, + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=1), + ), + EnergyProvider( + organization=organization, + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=1), + ), + ) + + def test_comparison(self) -> None: + organization = self._organization() + + self.assertTrue( + EnergyProvider( + organization=organization, + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=1), + ) + <= EnergyProvider( + organization=organization, + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=1), + ) + ) + + def test_hash(self) -> None: + organization = self._organization() + + self.assertEqual( + hash(EnergyProvider( + organization=organization, + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=1), + )), + hash(EnergyProvider( + organization=organization, + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=1), + )), + ) + + def test_repr(self) -> None: + self.assertIn( + 'EnergyProvider', + repr(EnergyProvider( + organization=self._organization(), + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=1), + )), + ) + + +class TestEnergyConsumption(TestCase): + + def _provider(self) -> EnergyProvider: + return EnergyProvider( + organization=OrganizationalEntity(name='provider'), + energy_source=EnergySource.SOLAR, + energy_provided=EnergyMeasure(value=10), + ) + + def test_constructor(self) -> None: + obj = EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[self._provider()], + activity_energy_cost=EnergyMeasure(value=100), + ) + + self.assertEqual(obj.activity, EnergyActivity.TRAINING) + self.assertEqual(len(obj.energy_providers), 1) + + def test_optional_co2_values(self) -> None: + obj = EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[self._provider()], + activity_energy_cost=EnergyMeasure(value=1), + co2_cost_equivalent=Co2Measure(value=1), + co2_cost_offset=Co2Measure(value=2), + ) + + self.assertEqual(obj.co2_cost_equivalent, Co2Measure(value=1)) + self.assertEqual(obj.co2_cost_offset, Co2Measure(value=2)) + + def test_properties(self) -> None: + prop = Property(name='key', value='value') + + obj = EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[self._provider()], + activity_energy_cost=EnergyMeasure(value=1), + properties=[prop], + ) + + self.assertIn(prop, obj.properties) + + def test_equality(self) -> None: + provider = self._provider() + + self.assertEqual( + EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[provider], + activity_energy_cost=EnergyMeasure(value=1), + ), + EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[provider], + activity_energy_cost=EnergyMeasure(value=1), + ), + ) + + def test_comparison(self) -> None: + provider = self._provider() + + self.assertTrue( + EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[provider], + activity_energy_cost=EnergyMeasure(value=1), + ) + <= EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[provider], + activity_energy_cost=EnergyMeasure(value=1), + ) + ) + + def test_hash(self) -> None: + provider = self._provider() + + self.assertEqual( + hash(EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[provider], + activity_energy_cost=EnergyMeasure(value=1), + )), + hash(EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[provider], + activity_energy_cost=EnergyMeasure(value=1), + )), + ) + + def test_repr(self) -> None: + self.assertIn( + 'EnergyConsumption', + repr(EnergyConsumption( + activity=EnergyActivity.TRAINING, + energy_providers=[], + activity_energy_cost=EnergyMeasure(value=1), + )), + ) + + +class TestConsiderations(TestCase): + + def test_defaults(self) -> None: + obj = Considerations() + + self.assertEqual(len(obj.users), 0) + self.assertEqual(len(obj.use_cases), 0) + self.assertIsNone(obj.environmental_considerations) + + def test_constructor(self) -> None: + obj = Considerations( + users=['developer'], + use_cases=['classification'], + technical_limitations=['latency'], + performance_tradeoffs=['accuracy'], + ) + + self.assertIn('developer', obj.users) + self.assertIn('classification', obj.use_cases) + + def test_sorted_values(self) -> None: + obj = Considerations(users=['b', 'a']) + + self.assertEqual( + list(obj.users), + ['a', 'b'], + ) + + def test_equality(self) -> None: + self.assertEqual( + Considerations(), + Considerations(), + ) + + def test_hash(self) -> None: + self.assertEqual( + hash(Considerations()), + hash(Considerations()), + ) + + def test_repr(self) -> None: + self.assertIn( + 'Considerations', + repr(Considerations()), + ) + + +class TestModelCard(TestCase): + + def test_defaults(self) -> None: + obj = ModelCard() + + self.assertIsNotNone(obj.bom_ref) + self.assertIsNone(obj.model_parameters) + self.assertIsNone(obj.quantitative_analysis) + self.assertIsNone(obj.considerations) + self.assertEqual(len(obj.properties), 0) + + def test_constructor(self) -> None: + model_parameters = ModelParameters(task='classification') + quantitative_analysis = QuantitativeAnalysis() + considerations = Considerations() + + obj = ModelCard( + model_parameters=model_parameters, + quantitative_analysis=quantitative_analysis, + considerations=considerations, + ) + + self.assertEqual(obj.model_parameters, model_parameters) + self.assertEqual(obj.quantitative_analysis, quantitative_analysis) + self.assertEqual(obj.considerations, considerations) + + def test_property_setters(self) -> None: + obj = ModelCard() + + value = Considerations() + + obj.considerations = value + + self.assertEqual(obj.considerations, value) + + def test_properties(self) -> None: + prop = Property(name='key', value='value') + + obj = ModelCard(properties=[prop]) + + self.assertIn(prop, obj.properties) + + def test_bom_ref(self) -> None: + obj = ModelCard( + bom_ref='model-card-ref', + ) + + self.assertEqual( + obj.bom_ref.value, + 'model-card-ref', + ) + + def test_equality(self) -> None: + self.assertEqual( + ModelCard(), + ModelCard(), + ) + + def test_comparison(self) -> None: + self.assertTrue( + ModelCard() <= ModelCard() + ) + + def test_hash(self) -> None: + self.assertEqual( + hash(ModelCard()), + hash(ModelCard()), + ) + + def test_repr(self) -> None: + self.assertIn( + 'ModelCard', + repr(ModelCard()), + )