# Copyright (c) 2024-2026 Karana Dynamics Pty Ltd. All rights reserved.
#
# NOTICE TO USER:
#
# This source code and/or documentation (the "Licensed Materials") is
# the confidential and proprietary information of Karana Dynamics Inc.
# Use of these Licensed Materials is governed by the terms and conditions
# of a separate software license agreement between Karana Dynamics and the
# Licensee ("License Agreement"). Unless expressly permitted under that
# agreement, any reproduction, modification, distribution, or disclosure
# of the Licensed Materials, in whole or in part, to any third party
# without the prior written consent of Karana Dynamics is strictly prohibited.
#
# THE LICENSED MATERIALS ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
# KARANA DYNAMICS DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND
# FITNESS FOR A PARTICULAR PURPOSE.
#
# IN NO EVENT SHALL KARANA DYNAMICS BE LIABLE FOR ANY DAMAGES WHATSOEVER,
# INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA, OR USE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, WHETHER IN CONTRACT, TORT,
# OR OTHERWISE ARISING OUT OF OR IN CONNECTION WITH THE LICENSED MATERIALS.
#
# U.S. Government End Users: The Licensed Materials are a "commercial item"
# as defined at 48 C.F.R. 2.101, and are provided to the U.S. Government
# only as a commercial end item under the terms of this license.
#
# Any use of the Licensed Materials in individual or commercial software must
# include, in the user documentation and internal source code comments,
# this Notice, Disclaimer, and U.S. Government Use Provision.
"""KModel Python base class."""
from numpy.typing import NDArray as _NDArray
import numpy as _np
from Karana.Models._KModel_pybind11_Py import *
from Karana.KUtils.DataStruct import DataStruct as _DataStruct
from Karana.Core import DumpOptionsBase as _DumpOptionsBase
from typing import TYPE_CHECKING, TypeVar, Generic
if TYPE_CHECKING:
from Karana.Dynamics import ModelManager as _ModelManager
class KModelParams(_DataStruct):
"""BaseClass for the model parameters of Python models.
For a new model parameter class, simply derive from this class
and add class variables just like you would do for any other
DataStruct. Users should override the isReady if applicable
to ensure that all model parameters have been defined.
"""
[docs]
def isReady(self) -> bool:
"""Return true if all the parameters are ready. False otherwise."""
return True
[docs]
def dumpString(self, prefix: str, options: _DumpOptionsBase | None = None) -> str:
"""Dump the parameters as a string.
Parameters
----------
prefix : str
The prefix for the string.
options : _DumpOptionsBase | None
Options for the string.
Returns
-------
str
The parameters as a string.
"""
return ""
class NoParams(KModelParams):
"""Class indicating there are no model params."""
pass
class NoScratch(KModelScratch):
"""Class indicating there is no model scratch."""
pass
class NoDiscreteStates(KModelDiscreteStates):
"""Class indicating there are no model discrete states."""
pass
class NoContinuousStates(KModelContinuousStates):
"""Class indicating there are no model continuous states."""
pass
P = TypeVar("P", bound=KModelParams)
Sc = TypeVar("Sc", bound=KModelScratch)
S = TypeVar("S", bound=KModelDiscreteStates)
C = TypeVar("C", bound=KModelContinuousStates)
class KModel(Generic[P, Sc, S, C], PyKModelBase):
"""Base class for all Python models.
This is the base class for all Python models. To create a new model,
simply create a class that derives from this one.
If the model has parameters, then create a parameter class that derives from
`KModelParams`. In addition, add
```
params: MyDerivedParamClass
```
as a class variable to ensure type-hinting works as intended.
Users can override the pre/postDeriv, pre/postHop, and pre/PostModelStep
to run model functions at various points throughout the dynamics. For details
on these methods and models, see the model documentation.
"""
params: P
scratch: Sc
discrete_states: S
continuous_states: C
def __init__(self, name: str, mm: "_ModelManager"):
"""Create KModel class."""
super().__init__(name, mm)
[docs]
def isReady(self) -> bool:
"""Return True if the model's parameters are ready. False otherwise."""
ready = True
if hasattr(self, "params") and self.params:
if not self.params.isReady():
ready = False
if self.__class__.preModelStep is not PyKModelBase.preModelStep:
from Karana.Core import warn
from Karana.Dynamics import StatePropagator as _StatePropagator
if isinstance(self.model_manager, _StatePropagator):
if not self.model_manager.hasRegisteredTimedEvent(
f"{self.name()}_pre_model_step_{self.id()}", True
):
warn(
"A preModelStep method is defined, but the period is 0 nor does the model have a nextModelStepTime method that returns a Ktime. Therefore, this method will never be run."
)
ready = False
if self.__class__.postModelStep is not PyKModelBase.postModelStep:
from Karana.Core import warn
from Karana.Dynamics import StatePropagator as _StatePropagator
if isinstance(self.model_manager, _StatePropagator):
if not self.model_manager.hasRegisteredTimedEvent(
f"{self.name()}_post_model_step_{self.id()}", False
):
warn(
"A postModelStep method is defined, but the period is 0 nor does the model have a nextModelStepTime method that returns a Ktime. Therefore, this method will never be run."
)
ready = False
return ready
def _getContinuousStates(self) -> _NDArray[_np.float64]:
"""Return the continuous states of the KModel.
Returns
-------
_NDArray[_np.float64]
The continuous states of the model.
"""
return self.continuous_states.getX()
def _getContinuousStatesDeriv(self) -> _NDArray[_np.float64]:
"""Return the continuous states' derivatives of the KModel.
Returns
-------
_NDArray[_np.float64]
The continuous states of the model.
"""
return self.continuous_states.getdX()
def _setContinuousStates(self, x: _NDArray[_np.float64]):
"""Set the continuous states of the KModel.
Parameters
----------
x : _NDArray[_np.float64]
Value to set the continuous states to.
"""
return self.continuous_states.setX(x)
def _registerModel(self):
# Check that classes are all of the correct type
# We do this here, since these are not necessarily set when the constructor runs.
if hasattr(self, "params") and (
self.params.__class__ is KModelParams or not isinstance(self.params, KModelParams)
):
raise ValueError(
"The model params should be derived from KModelParams, but must not be KModelParams itself. Use NoParams if your model does not have any model params."
)
if hasattr(self, "scratch") and (
self.scratch.__class__ is KModelScratch or not isinstance(self.scratch, KModelScratch)
):
raise ValueError(
"The model scratch should be derived from KModelScratch, but must not be KModelScratch itself. Use NoScratch if your model does not have any scratch."
)
if hasattr(self, "discrete_states") and (
self.discrete_states.__class__ is KModelDiscreteStates
or not isinstance(self.discrete_states, KModelDiscreteStates)
):
raise ValueError(
"The dicrete model states should be derived from KModelDiscreteStates, but must not be KModelDiscreteStates itself. Use NoDiscreteStates if your model does not have any dicrete states."
)
if hasattr(self, "continuous_states") and (
self.continuous_states.__class__ is KModelContinuousStates
or not isinstance(self.continuous_states, KModelContinuousStates)
):
raise ValueError(
"The continuous model states should be derived from KModelContinuousStates, but must not be KModelContinuousStates itself. Use NoContinuousStates if your model does not have any continuous states."
)
# Register the model
super()._registerModel()
# Set the _has_continuous_states value via the _setHasContinuousStates Python-only method.
# This must be done in _registerModel, so these states get added to the integrator if they are present.
if (
hasattr(self, "continuous_states")
and self.continuous_states.__class__ is not NoContinuousStates
):
# If we have continuous states, then set the boolean appropriately
self._setHasContinuousStates(True)
else:
self._setHasContinuousStates(False)
[docs]
def typeString(self) -> str:
"""Return the type string (class name) of the KModel.
Returns
-------
str
The class name of the KModel.
"""
return self.__class__.__name__
[docs]
def dumpString(self, prefix: str = "", options: _DumpOptionsBase | None = None) -> str:
"""Return information about the KModel as a string.
Parameters
----------
prefix : str
A string to use as prefix for each output line.
options : _DumpOptionsBase | None
Class with options to tailor the output
Returns
-------
str
Information about the KModel as a string.
"""
result = f"{prefix}Dumping model '{self.name()}' ({self.typeString()})\n"
# If we have params, add those to the dumpString
if hasattr(self, "params") and not isinstance(self.params, NoParams):
result += f"{prefix} Model Params:\n"
result += self.params.dumpString(f"{prefix} ")
# If we have scratch, add those to the dumpString
if hasattr(self, "scratch") and not isinstance(self.scratch, NoScratch):
result += f"{prefix} Model Scratch:\n"
result += self.scratch.dumpString(f"{prefix} ")
# If we have discrete states, add those to the dumpString
if hasattr(self, "discrete_states") and not isinstance(
self.discrete_states, NoDiscreteStates
):
result += f"{prefix} Model Discrete States:\n"
result += self.discrete_states.dumpString(f"{prefix} ")
# If we have continuous states, add those to the dumpString
if hasattr(self, "continuous_states") and not isinstance(
self.continuous_states, NoDiscreteStates
):
result += f"{prefix} Model Continuous States:\n"
result += self.continuous_states.dumpString(f"{prefix} ")
return result