Skip to content

Version 1

The version 1 models are based on modeling the domain as an asset hierarchy for the production and market configurations. While CogSHOP is based on an early version of data models. In the second version, all configuration will move to the newest version of the data models.

Production Model

cognite.powerops.resync.models.v1.ProductionModel

Bases: AssetModel

Source code in cognite/powerops/resync/models/v1/production.py
class ProductionModel(AssetModel):
    root_asset: ClassVar[Asset] = Asset(external_id="power_ops", name="PowerOps")
    plants: list[Plant] = Field(default_factory=list)
    generators: list[Generator] = Field(default_factory=list)
    reservoirs: list[Reservoir] = Field(default_factory=list)
    watercourses: list[Watercourse] = Field(default_factory=list)
    price_areas: list[PriceArea] = Field(default_factory=list)

    @field_validator("plants", "generators", "reservoirs", "watercourses", "price_areas", mode="after")
    @classmethod
    def ordering(cls, value: list[T_Asset_Type]) -> list[T_Asset_Type]:
        # To ensure loading the production model always yields the same result, we sort the assets by external_id.
        return sorted(value, key=lambda x: x.external_id)

    def standardize(self) -> None:
        self.plants = self.ordering(self.plants)
        self.generators = self.ordering(self.generators)
        self.reservoirs = self.ordering(self.reservoirs)
        self.watercourses = self.ordering(self.watercourses)
        self.price_areas = self.ordering(self.price_areas)
        for field in [self.plants, self.generators, self.reservoirs, self.watercourses, self.price_areas]:
            for item in field:
                item.standardize()

cognite.powerops.resync.models.v1.PriceArea

Bases: AssetType

Source code in cognite/powerops/resync/models/v1/production.py
class PriceArea(AssetType):
    parent_external_id: ClassVar[str] = "price_areas"
    label: ClassVar[Union[AssetLabel, str]] = AssetLabel.PRICE_AREA
    dayahead_price_time_series: Optional[TimeSeries] = None
    plants: list[Plant] = Field(default_factory=list)
    watercourses: list[Watercourse] = Field(default_factory=list)

    @field_validator("plants", "watercourses", mode="after")
    @classmethod
    def ordering(cls, value: list[T_Asset_Type]) -> list[T_Asset_Type]:
        # To ensure loading the production model always yields the same result, we sort the assets by external_id.
        return sorted(value, key=lambda x: x.external_id)

    def standardize(self) -> None:
        self.plants = self.ordering(self.plants)
        self.watercourses = self.ordering(self.watercourses)

    @field_serializer("dayahead_price_time_series")
    def ser_time_series(self, value) -> dict[str, Any]:
        if value is None:
            return {}
        return {"externalId": value.external_id}

cognite.powerops.resync.models.v1.Watercourse

Bases: AssetType

Source code in cognite/powerops/resync/models/v1/production.py
class Watercourse(AssetType):
    model_config: ClassVar[ConfigDict] = ConfigDict(protected_namespaces=tuple())
    parent_external_id: ClassVar[str] = "watercourses"
    label: ClassVar[Union[AssetLabel, str]] = AssetLabel.WATERCOURSE
    shop: WaterCourseShop
    config_version: Optional[str] = Field("", exclude=True)
    model_file: Optional[Path] = Field(None, exclude=True)
    processed_model_file: Optional[Path] = Field(None, exclude=True)
    plants: list[Plant] = Field(default_factory=list)
    production_obligation_time_series: list[TimeSeries] = Field(default_factory=list)
    write_back_model_file: Optional[bool] = Field(True, exclude=True)

    @field_validator("plants", mode="after")
    @classmethod
    def plant_ordering(cls, value: list[Plant]) -> list[Plant]:
        # To ensure loading the production model, always yields the same result; we sort the plants by external_id.
        return sorted(value, key=lambda x: x.external_id)

    def standardize(self) -> None:
        self.plants = self.plant_ordering(self.plants)

    @field_validator("production_obligation_time_series", mode="before")
    @classmethod
    def none_to_empty_list(cls, value) -> list[TimeSeries]:
        if value is None or (isinstance(value, list) and value and value[0] is None):
            return []
        return value

    @field_serializer("production_obligation_time_series")
    def ser_time_series(self, value) -> dict[str, Any]:
        if value is None:
            return []
        return [{"externalId": ts.external_id} for ts in value]

cognite.powerops.resync.models.v1.Plant

Bases: AssetType

Source code in cognite/powerops/resync/models/v1/production.py
class Plant(AssetType):
    parent_external_id: ClassVar[str] = "plants"
    label: ClassVar[Union[AssetLabel, str]] = AssetLabel.PLANT
    display_name: str
    ordering: str
    head_loss_factor: float
    outlet_level: float
    p_min: float
    p_max: float
    penstock_head_loss_factors: dict = Field(default_factory=dict)
    generators: list[Generator] = Field(default_factory=list)
    connection_losses: Optional[float] = Field(default=0.0)
    inlet_reservoir: Optional[Reservoir] = None
    p_min_time_series: Optional[TimeSeries] = None
    p_max_time_series: Optional[TimeSeries] = None
    water_value_time_series: Optional[TimeSeries] = None
    feeding_fee_time_series: Optional[TimeSeries] = None
    outlet_level_time_series: Optional[TimeSeries] = None
    inlet_level_time_series: Optional[TimeSeries] = None
    head_direct_time_series: Optional[TimeSeries] = None

    @field_validator("generators", mode="after")
    @classmethod
    def generator_ordering(cls, value: list[Generator]) -> list[Generator]:
        # To ensure loading the production model always yields the same result, we sort the generators by external_id.
        return sorted(value, key=lambda x: x.external_id)

    def standardize(self) -> None:
        self.generators = self.generator_ordering(self.generators)

    @field_validator("penstock_head_loss_factors", mode="before")
    @classmethod
    def parse_str(cls, value) -> dict:
        return try_load_dict(value)

    @field_serializer(
        "p_min_time_series",
        "p_max_time_series",
        "water_value_time_series",
        "feeding_fee_time_series",
        "outlet_level_time_series",
        "inlet_level_time_series",
        "head_direct_time_series",
    )
    def ser_time_series(self, value) -> dict[str, Any]:
        if value is None:
            return {}
        return {"externalId": value.external_id}

    @field_validator(
        "p_min_time_series",
        "p_max_time_series",
        "water_value_time_series",
        "feeding_fee_time_series",
        "outlet_level_time_series",
        "inlet_level_time_series",
        "head_direct_time_series",
        mode="before",
    )
    @classmethod
    def parse_timeseries(cls, value):
        return parse_time_series(value)

cognite.powerops.resync.models.v1.Reservoir

Bases: AssetType

Source code in cognite/powerops/resync/models/v1/production.py
class Reservoir(AssetType):
    parent_external_id: ClassVar[str] = "reservoirs"
    label: ClassVar[Union[AssetLabel, str]] = AssetLabel.RESERVOIR
    display_name: str
    ordering: str

cognite.powerops.resync.models.v1.Generator

Bases: AssetType

Source code in cognite/powerops/resync/models/v1/production.py
class Generator(AssetType):
    parent_external_id: ClassVar[str] = "generators"
    label: ClassVar[Union[AssetLabel, str]] = AssetLabel.GENERATOR
    p_min: float
    penstock: str
    startcost: float
    start_stop_cost_time_series: Optional[TimeSeries] = None
    is_available_time_series: Optional[TimeSeries] = None
    generator_efficiency_curve: Optional[CDFSequence] = None
    turbine_efficiency_curve: Optional[CDFSequence] = None

    @field_serializer("start_stop_cost_time_series", "is_available_time_series")
    def ser_time_series(self, value) -> dict[str, Any]:
        if value is None:
            return {}
        return {"externalId": value.external_id}

    @field_validator("generator_efficiency_curve", "turbine_efficiency_curve", mode="before")
    @classmethod
    def parse_sequences(cls, value):
        if value == {}:
            return None
        return value

    @field_validator("start_stop_cost_time_series", "is_available_time_series", mode="before")
    @classmethod
    def parse_timeseries(cls, value):
        return parse_time_series(value)

Market Model

cognite.powerops.resync.models.v1.MarketModel

Bases: AssetModel

Source code in cognite/powerops/resync/models/v1/market/__init__.py
class MarketModel(AssetModel):
    root_asset: ClassVar[Optional[Asset]] = None
    nordpool_market: list[NordPoolMarket] = Field(default_factory=list)
    rkom_market: list[RKOMMarket] = Field(default_factory=list)
    dayahead_processes: list[DayAheadProcess] = Field(default_factory=list)
    benchmark_processes: list[BenchmarkProcess] = Field(default_factory=list)
    rkom_processes: list[RKOMProcess] = Field(default_factory=list)
    combinations: list[RKOMBidCombination] = Field(default_factory=list)

    @field_validator(
        "nordpool_market",
        "rkom_market",
        "dayahead_processes",
        "benchmark_processes",
        "rkom_processes",
        "combinations",
        mode="after",
    )
    @classmethod
    def ordering(cls, value: list[T_Asset_Type]) -> list[T_Asset_Type]:
        # To ensure loading the production model always yields the same result, we sort the assets by external_id.
        return sorted(value, key=lambda x: x.external_id)

    @classmethod
    def set_root_asset(
        cls, shop_service_url: str, organization_subdomain: str, tenant_id: str, core_root_asset_external_id: str
    ) -> None:
        if shop_service_url == "https://shop-staging.az-inso-powerops.cognite.ai/submit-run":
            customer = "cognite"
        else:
            customer = organization_subdomain

        cls.root_asset = Asset(
            parent_external_id=core_root_asset_external_id,
            external_id="configurations",
            name="Configurations",
            description="Configurations used for PowerOps",
            metadata={
                "shop_service_url": shop_service_url,
                "organization_subdomain": organization_subdomain,
                "customer": customer,
                "tenant_id": tenant_id,
            },
        )

    def standardize(self) -> None:
        self.dayahead_processes = self.ordering(self.dayahead_processes)
        self.benchmark_processes = self.ordering(self.benchmark_processes)
        self.rkom_processes = self.ordering(self.rkom_processes)
        self.combinations = self.ordering(self.combinations)
        for field in [
            self.dayahead_processes,
            self.benchmark_processes,
            self.rkom_processes,
            self.combinations,
            self.nordpool_market,
            self.rkom_market,
        ]:
            for item in field:
                item.standardize()

    @property
    def processes(self):
        return self.dayahead_processes + self.benchmark_processes + self.rkom_processes

cognite.powerops.resync.models.v1.BenchmarkProcess

Bases: Process

Source code in cognite/powerops/resync/models/v1/market/benchmark.py
class BenchmarkProcess(Process):
    parent_external_id: ClassVar[str] = "benchmarking_configurations"
    parent_description: ClassVar[str] = "Configurations used in benchmarking processes"
    description: str = "Configuration for benchmarking of day-ahead bidding"
    label: ClassVar[Union[AssetLabel, str]] = AssetLabel.DAYAHEAD_BIDDING_BENCHMARKING_CONFIG
    shop: ShopTransformation
    bid: BenchmarkBid
    production_plan_time_series: list[ProductionPlanTimeSeries] = Field(default_factory=list)
    benchmarking_metrics: dict[str, str] = Field(default_factory=dict)
    run_events: list[str] = Field(default_factory=list)
    bid_process_configuration_assets: list[Process] = Field(default_factory=list)

    @field_validator("production_plan_time_series", mode="after")
    @classmethod
    def ordering(cls, value: list[ProductionPlanTimeSeries]) -> list[ProductionPlanTimeSeries]:
        return sorted(value, key=lambda x: x.name)

    @field_validator("run_events", mode="after")
    @classmethod
    def ordering_events(cls, value: list[str]) -> list[str]:
        return sorted(value)

    @field_validator("bid_process_configuration_assets", mode="after")
    @classmethod
    def ordering_processes(cls, value: list[Process]) -> list[Process]:
        return sorted(value, key=lambda x: x.name)

    @field_validator("benchmarking_metrics", mode="before")
    @classmethod
    def parse_str(cls, value) -> dict:
        return try_load_dict(value)

    @field_validator("production_plan_time_series", mode="before")
    @classmethod
    def parse_str_to_list(cls, value) -> list:
        if isinstance(loaded := try_load_list(value), dict):
            return [loaded]
        return loaded

    def standardize(self) -> None:
        self.production_plan_time_series = self.ordering(self.production_plan_time_series)
        self.run_events = self.ordering_events(self.run_events)
        self.bid_process_configuration_assets = self.ordering_processes(self.bid_process_configuration_assets)

cognite.powerops.resync.models.v1.BenchmarkBid

Bases: Bid

Source code in cognite/powerops/resync/models/v1/market/benchmark.py
class BenchmarkBid(Bid):
    market_config_external_id: str

cognite.powerops.resync.models.v1.NordPoolMarket

Bases: Market

Source code in cognite/powerops/resync/models/v1/market/dayahead.py
class NordPoolMarket(Market):
    # Temporary optional to fix that this is missing in CDF.
    max_price: Optional[float] = None
    min_price: Optional[float] = None
    price_steps: Optional[int] = None
    price_unit: Optional[str] = None
    tick_size: Optional[float] = None
    time_unit: Optional[str] = None
    trade_lot: Optional[float] = None

cognite.powerops.resync.models.v1.DayAheadProcess

Bases: Process

Source code in cognite/powerops/resync/models/v1/market/dayahead.py
class DayAheadProcess(Process):
    parent_external_id = "bid_process_configurations"
    label: ClassVar[Union[AssetLabel, str]] = AssetLabel.BID_PROCESS_CONFIGURATION
    parent_description: ClassVar[str] = "Configurations used in bid matrix generation processes"
    shop: ShopTransformation
    bid: DayAheadBid
    bid_matrix_generator_config: Optional[CDFSequence] = None
    incremental_mapping: list[CDFSequence] = Field(default_factory=list)

    @field_validator("incremental_mapping", mode="after")
    @classmethod
    def ordering(cls, value: list[CDFSequence]) -> list[CDFSequence]:
        return sorted(value, key=lambda x: x.external_id)

    @field_validator("bid", mode="before")
    @classmethod
    def parse_str(cls, value) -> dict:
        return try_load_dict(value)

    @property
    def watercourses(self) -> list[str]:
        return list({i.sequence.metadata.get("shop:watercourse") for i in self.incremental_mapping})

    def standardize(self) -> None:
        self.bid.standardize()
        self.incremental_mapping = self.ordering(self.incremental_mapping)

cognite.powerops.resync.models.v1.DayAheadBid

Bases: Bid

Source code in cognite/powerops/resync/models/v1/market/dayahead.py
class DayAheadBid(Bid):
    is_default_config_for_price_area: bool = True
    main_scenario: str
    price_area: str
    price_scenarios: dict[str, str]
    no_shop: bool = False
    bid_process_configuration_name: str
    bid_matrix_generator_config_external_id: str
    market_config_external_id: str

    @field_validator("price_scenarios", mode="before")
    @classmethod
    def parse_str(cls, value) -> dict:
        value = try_load_dict(value)
        if isinstance(value, list) and not value:
            return {}
        return value

    def standardize(self): ...

cognite.powerops.resync.models.v1.RKOMProcess

Bases: Process

Source code in cognite/powerops/resync/models/v1/market/rkom.py
class RKOMProcess(Process):
    parent_external_id: ClassVar[str] = "rkom_bid_process_configurations"
    label: ClassVar[Union[AssetLabel, str]] = AssetLabel.RKOM_BID_CONFIGURATION
    parent_description: ClassVar[str] = "Configurations used in RKOM bid generation processes"
    shop: ShopTransformation
    bid: RKOMBid
    process_events: list[str] = Field(default_factory=list)
    timezone: str
    rkom: RKOMPlants
    incremental_mapping: list[CDFSequence] = Field(default_factory=list)

    @field_validator("incremental_mapping", mode="after")
    @classmethod
    def ordering(cls, value: list[CDFSequence]) -> list[CDFSequence]:
        return sorted(value, key=lambda x: x.external_id)

    @field_validator("process_events", mode="before")
    @classmethod
    def ordering_events(cls, value: list[str]) -> list[str]:
        return sorted(value)

    @field_validator("process_events", mode="before")
    @classmethod
    def parse_str(cls, value) -> list:
        return try_load_list(value)

    def standardize(self) -> None:
        self.incremental_mapping = self.ordering(self.incremental_mapping)
        self.process_events = self.ordering_events(self.process_events)

cognite.powerops.resync.models.v1.RKOMBid

Bases: Bid

Source code in cognite/powerops/resync/models/v1/market/rkom.py
class RKOMBid(Bid):
    auction: str
    block: str
    method: str
    minimum_price: str
    price_premium: str
    product: str
    watercourse: str
    price_scenarios: str
    reserve_scenarios: str

cognite.powerops.resync.models.v1.RKOMBidCombination

Bases: AssetType

Source code in cognite/powerops/resync/models/v1/market/rkom.py
class RKOMBidCombination(AssetType):
    parent_external_id: ClassVar[str] = "rkom_bid_combination_configurations"
    label: ClassVar[str] = AssetLabel.RKOM_BID_CONFIGURATION
    parent_description: ClassVar[str] = "Configurations for which bids should be combined into a total RKOM bid form"
    bid: RKOMCombinationBid

    def standardize(self) -> None:
        self.bid.standardize()

cognite.powerops.resync.models.v1.RKOMCombinationBid

Bases: NonAssetType

Source code in cognite/powerops/resync/models/v1/market/rkom.py
class RKOMCombinationBid(NonAssetType):
    auction: str
    combination_name: str
    rkom_bid_configs: list[str]

    @field_validator("rkom_bid_configs", mode="before")
    @classmethod
    def parse_str(cls, value) -> list:
        return try_load_list(value)

    @field_validator("rkom_bid_configs", mode="after")
    @classmethod
    def ordering(cls, value: list[str]) -> list[str]:
        return sorted(value)

    def standardize(self) -> None:
        self.rkom_bid_configs = self.ordering(self.rkom_bid_configs)

cognite.powerops.resync.models.v1.RKOMMarket

Bases: Market

Source code in cognite/powerops/resync/models/v1/market/rkom.py
class RKOMMarket(Market):
    # Temporary optional to fix that this is missing in CDF.
    start_of_week: Optional[int] = None

CogSHOP Model

cognite.powerops.resync.models.v1.CogShop1Asset

Bases: CogShopCore, DataModel

Source code in cognite/powerops/resync/models/v1/cogshop.py
class CogShop1Asset(CogShopCore, DataModel, protected_namespaces=()):
    cls_by_container: ClassVar[dict[ContainerId, type[DomainModelApplyCogShop1]]] = {
        ContainerId("cogShop", "ModelTemplate"): cogshop_v1.ModelTemplateApply,
        ContainerId("cogShop", "Mapping"): cogshop_v1.MappingApply,
        ContainerId("cogShop", "FileRef"): cogshop_v1.FileRefApply,
        ContainerId("cogShop", "Transformation"): cogshop_v1.TransformationApply,
        ContainerId("cogShop", "Scenario"): cogshop_v1.ScenarioApply,
        ContainerId("cogShop", "CommandsConfig"): cogshop_v1.CommandsConfigApply,
    }
    graph_ql: ClassVar[PowerOpsGraphQLModel] = cogshop1_graphql
    model_templates: dict[ExternalID, cogshop_v1.ModelTemplateApply] = Field(default_factory=dict)
    mappings: dict[ExternalID, cogshop_v1.MappingApply] = Field(default_factory=dict)
    transformations: dict[ExternalID, cogshop_v1.TransformationApply] = Field(default_factory=dict)
    base_mappings: list[CDFSequence] = Field(default_factory=list)
    output_definitions: list[CDFSequence] = Field(default_factory=list)
    scenarios: dict[ExternalID, cogshop_v1.ScenarioApply] = Field(default_factory=dict)
    commands_configs: dict[ExternalID, cogshop_v1.CommandsConfigApply] = Field(default_factory=dict)

    @field_validator("base_mappings", mode="after")
    @classmethod
    def ordering_sequences(cls, value: list[CDFSequence]) -> list[CDFSequence]:
        return sorted(value, key=lambda x: x.external_id)

    @field_validator("model_templates", "mappings", "transformations", mode="after")
    @classmethod
    def ordering_dict(cls, value: dict) -> dict:
        return {k: v for k, v in sorted(value.items(), key=lambda x: x[0])}

    @classmethod
    def from_cdf(cls, client: PowerOpsClient, data_set_external_id: str) -> CogShop1Asset:
        cog_shop = client.cog_shop1
        templates = cog_shop.model_template.list(limit=-1, source="resync")
        scenarios = cog_shop.scenario.list(limit=-1, source="resync")

        base_mapping_ids = list(
            {m for t in templates for m in t.base_mappings or []}
            | {m for s in scenarios for m in s.mappings_override or []}
        )

        if base_mapping_ids:
            base_mappings = cog_shop.mapping.retrieve(external_id=base_mapping_ids)
        else:
            base_mappings = []

        if transformation_ids := list({t for m in base_mappings for t in m.transformations or []}):
            transformations = cog_shop.transformation.retrieve(transformation_ids)
        else:
            transformations = []

        if file_ids := list({t.model for t in templates} | {f for s in scenarios for f in s.extra_files or []}):
            files = cog_shop.file_ref.retrieve(file_ids)
        else:
            files = []

        if command_ids := list({s.commands for s in scenarios}):
            commands_configs = cog_shop.commands_config.retrieve(command_ids)
        else:
            commands_configs = []

        transformation_by_id: dict[str, cogshop_v1.TransformationApply] = {
            t.external_id: t.as_apply() for t in transformations
        }

        mappings_by_id: dict[str, cogshop_v1.MappingApply] = {}
        for mapping in base_mappings:
            apply = mapping.as_apply()
            apply.transformations = sorted(
                (transformation_by_id[t] for t in apply.transformations or [] if t in transformation_by_id),
                key=lambda x: x.order,
            )
            mappings_by_id[apply.external_id] = apply

        command_configs_by_id = {commands.external_id: commands.as_apply() for commands in commands_configs}

        file_by_id: dict[str, cogshop_v1.FileRefApply] = {f.external_id: f.as_apply() for f in files}
        model_templates: dict[str, cogshop_v1.ModelTemplateApply] = {}
        for template in templates:
            apply = template.as_apply()
            if apply.model in file_by_id:
                apply.model = file_by_id[apply.model]
            apply.base_mappings = [mappings_by_id[m] for m in apply.base_mappings or [] if m in mappings_by_id]
            model_templates[apply.external_id] = apply

        scenarios_by_id = {}
        for scenario in scenarios:
            apply = scenario.as_apply()
            apply.mappings_override = [mappings_by_id[m] for m in apply.mappings_override or []]
            apply.commands = command_configs_by_id.get(apply.commands)
            apply.model_template = model_templates.get(apply.model_template)
            scenarios_by_id[apply.external_id] = apply

        # There files and sequences are not linked to the model templates
        # (this should be done in the next version of Cogshop).
        watercourse_names = list({t.watercourse for t in model_templates.values()})
        sequence_ids = [
            f"SHOP_{name}{suffix}"
            for name, suffix in product(watercourse_names, ["_base_mapping", "_output_definition"])
        ]
        cdf_client: CogniteClient = client.cdf
        if sequence_ids:
            sequences = cdf_client.sequences.retrieve_multiple(external_ids=sequence_ids)
        else:
            all_sequences = cdf_client.sequences.list(limit=-1, external_id_prefix="SHOP_")
            sequences = [
                s
                for s in all_sequences
                if s.external_id.endswith("_base_mapping") or s.external_id.endswith("_output_definition")
            ]

        base_mappings = [CDFSequence(sequence=s) for s in sequences if s.external_id.endswith("_base_mapping")]
        output_definitions = [
            CDFSequence(sequence=s) for s in sequences if s.external_id.endswith("_output_definition")
        ]

        files = cdf_client.files.list(limit=-1, source="PowerOps bootstrap", mime_type="text/plain")
        shop_files = [CDFFile(meta=f) for f in files]

        return cls(
            model_templates=model_templates,
            mappings=mappings_by_id,
            transformations=transformation_by_id,
            base_mappings=base_mappings,
            output_definitions=output_definitions,
            shop_files=shop_files,
            scenarios=scenarios_by_id,
            commands_configs=command_configs_by_id,
        )

    def standardize(self) -> None:
        super().standardize()
        self.base_mappings = self.ordering_sequences(self.base_mappings)
        self.output_definitions = self.ordering_sequences(self.output_definitions)
        self.model_templates = self.ordering_dict(self.model_templates)
        self.mappings = self.ordering_dict(self.mappings)
        self.transformations = self.ordering_dict(self.transformations)
        self.scenarios = self.ordering_dict(self.scenarios)
        self.commands_configs = self.ordering_dict(self.commands_configs)
        for scenario in self.scenarios.values():
            scenario.mappings_override = sorted(
                scenario.mappings_override or [], key=lambda x: x if isinstance(x, str) else x.external_id
            )

        for template in self.model_templates.values():
            template.base_mappings = sorted(
                template.base_mappings or [], key=lambda x: x if isinstance(x, str) else x.external_id
            )

        for mapping in self.mappings.values():
            mapping.transformations = sorted(
                mapping.transformations or [], key=lambda x: x[-1] if isinstance(x, str) else x.external_id
            )