Source code for Karana.KUtils._toPythonScript

# 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.

import os
import re
from contextlib import contextmanager
from typing import runtime_checkable, Protocol, TYPE_CHECKING, overload, cast
from pint import Quantity
import numpy as np
import Karana.Core as kc
from pathlib import Path

if TYPE_CHECKING:
    from Karana.KUtils.DataStruct import DataStruct, IdMixin
else:
    from .DataStruct import DataStruct, IdMixin


@runtime_checkable
class _Named(Protocol):
    name: str  # Any class matching this protocol must have a 'name' attribute that is a string


[docs] class VarPythonScriptContext: def __init__(self): self._names_dict: dict[object | int, str] = {} # Add reserved names self._names: set[str] = {"o_sh", "p_sh", "sh", "vroot"} # Common regexs used self._name_match_re = re.compile("[a-zA-Z_][a-zA-Z0-9_]+") self._rename_re = re.compile(r"(?<=[a-z])(?=[A-Z])|[^a-zA-Z0-9]") @overload def getVarNameIfExists(self, ds: DataStruct) -> str | None: ... @overload def getVarNameIfExists(self, obj: object) -> str | None: ...
[docs] def getVarNameIfExists(self, *args, **_): if isinstance(ds := args[0], DataStruct): # If this is already in the cache, then return it if body_name := self._names_dict.get(id(ds)): # Search by id return body_name elif isinstance(ds, IdMixin) and len(ds._objects_from_id) == 1: # Search by Base object if it exists obj = cast(kc.Base | None, ds._objects_from_id[0]()) if obj is not None and (obj_name := self._names_dict.get(obj, None)): # If found by object, add by ID as well self._names_dict[id(ds)] = obj_name # Return the object return obj_name else: return None else: obj = cast(object, args[0]) # If this is already in the names cache, then return it return self._names_dict.get(obj, None)
@overload def getVarName(self, ds: DataStruct, base_name: str = "var") -> str: ... @overload def getVarName(self, obj: object, base_name: str = "var") -> str: ...
[docs] def getVarName(self, *args, **kwargs): # Return the name if it already exists if name := self.getVarNameIfExists(*args, **kwargs): return name # Otherwise, create a new name if isinstance(ds := args[0], DataStruct): base_name = kwargs.get("base_name", "var") if isinstance(ds, _Named) and re.match(self._name_match_re, ds.name): # If the name of the body can become a variable, then use that dark = re.sub(self._rename_re, "_", ds.name).lower() else: # Otherwise, just use the base_name dark = base_name # Add on a number to make this unique if necessary body_name = dark k = 0 while body_name in self._names: k += 1 body_name = dark + f"_{k}" # Add the new name to the cache self._names.add(body_name) self._names_dict[id(ds)] = body_name if isinstance(ds, IdMixin) and len(ds._objects_from_id) == 1: body = ds._objects_from_id[0]() if body is not None: self._names_dict[body] = body_name return body_name else: base_name = kwargs.get("base_name", "var") if isinstance(base := args[0], kc.Base): base = cast(kc.Base, args[0]) if re.match(self._name_match_re, base.name()): # If the name of the body can become a variable, then use that dark = re.sub(self._rename_re, "_", base.name()).lower() else: # Otherwise, just use the base_name dark = base_name else: dark = base_name # Add on a number to make this unique if necessary obj_name = dark k = 0 while obj_name in self._names: k += 1 obj_name = dark + f"_{k}" # Add the new name to the cache self._names.add(obj_name) self._names_dict[base] = obj_name return obj_name
[docs] def valuePrinter(q) -> str: """Print values for Python scripts. Parameters ---------- q : Quantity | NDArray The value to print Returns ------- str The string representation of the value. """ if isinstance(q, Quantity): if isinstance(q.m, np.ndarray): return "np." + repr(q.m) + " * " + re.sub(r"([a-z]+)", r"ureg.\1", str(q.u)) elif isinstance(q.m, float): return repr(q.m) + " * " + re.sub(r"([a-z]+)", r"ureg.\1", str(q.u)) else: raise ValueError(f"Not sure how to print out quantity with magnitude type {type(q.m)}.") elif isinstance(q, np.ndarray): return "np." + repr(q) elif isinstance(q, float): return repr(q) else: raise ValueError("Type unknown.")
[docs] @contextmanager def fullPrintOpts(): """Context to print numpy values with full precision.""" opts = np.get_printoptions() try: np.set_printoptions(floatmode="unique") yield finally: np.set_printoptions(**opts)
def writePythonScript(code: str, file: Path): """Write the Python script to the provided Path. This adds comment seperators with the appropriate number of -'s. It also runs ruff on the output. """ comment_sep = re.compile(r"([ \t]+)_KARANA_COMMENT_SEPARATOR") def replaceCommentSep(match: re.Match) -> str: """Replace the incoming match with a comment separator. Parameters ---------- match : Math The regex match to fix. Returns ------- str The comment separator. """ indent = match.group(1) prefix = "# " dashes = "-" * (100 - len(indent) - len(prefix)) return f"{indent}{prefix}{dashes}" # Replace comment separators code = re.sub(comment_sep, replaceCommentSep, code) # Write the file with open(file, "w") as f: f.write(code) # Format the file with ruff wd = file.parent os.system(f"cd {wd} && ruff format --config 'line-length=100' {file}")