"""
Utility functions for loading ams stock test cases,
mainly revised from ``andes.utils.paths``.
"""
import logging
import os
import pathlib
import tempfile
logger = logging.getLogger(__name__)
[docs]
class DisplayablePath:
display_filename_prefix_middle = '├──'
display_filename_prefix_last = '└──'
display_parent_prefix_middle = ' '
display_parent_prefix_last = '│ '
[docs]
def __init__(self, path, parent_path, is_last):
self.path = pathlib.Path(str(path))
self.parent = parent_path
self.is_last = is_last
if self.parent:
self.depth = self.parent.depth + 1
else:
self.depth = 0
@property
def displayname(self):
if self.path.is_dir():
return self.path.name + '/'
return self.path.name
[docs]
@classmethod
def make_tree(cls, root, parent=None, is_last=False, criteria=None):
root = pathlib.Path(str(root))
criteria = criteria or cls._default_criteria
displayable_root = cls(root, parent, is_last)
yield displayable_root
children = sorted(list(path
for path in root.iterdir()
if criteria(path)),
key=lambda s: str(s).lower())
count = 1
for path in children:
is_last = count == len(children)
if path.is_dir():
yield from cls.make_tree(path,
parent=displayable_root,
is_last=is_last,
criteria=criteria)
else:
yield cls(path, displayable_root, is_last)
count += 1
@classmethod
def _default_criteria(cls, path):
return True
[docs]
def displayable(self):
if self.parent is None:
return self.displayname
_filename_prefix = (self.display_filename_prefix_last
if self.is_last
else self.display_filename_prefix_middle)
parts = ['{!s} {!s}'.format(_filename_prefix,
self.displayname)]
parent = self.parent
while parent and parent.parent is not None:
parts.append(self.display_parent_prefix_middle
if parent.is_last
else self.display_parent_prefix_last)
parent = parent.parent
return ''.join(reversed(parts))
[docs]
def ams_root():
"""
Return the root path to the ams source code.
"""
dir_name = os.path.dirname(os.path.abspath(__file__))
return os.path.normpath(os.path.join(dir_name, '..'))
[docs]
def cases_root():
"""
Return the root path to the stock cases
"""
dir_name = os.path.dirname(os.path.abspath(__file__))
return os.path.normpath(os.path.join(dir_name, '..', 'cases'))
[docs]
def tests_root():
"""Return the root path to the stock cases"""
dir_name = os.path.dirname(os.path.abspath(__file__))
return os.path.normpath(os.path.join(dir_name, '..', '..', 'tests'))
[docs]
def get_case(rpath, check=True):
"""
Return the path to a stock case for a given path relative to ``ams/cases``.
To list all cases, use ``ams.list_cases()``.
Parameters
----------
check : bool
True to check if file exists
Examples
--------
To get the path to the case `kundur_full.xlsx` under folder `kundur`, do ::
ams.get_case('kundur/kundur_full.xlsx')
"""
case_path = os.path.join(cases_root(), rpath)
case_path = os.path.normpath(case_path)
if check is True and (not os.path.isfile(case_path)):
raise FileNotFoundError(f'"{rpath}" is not a valid relative path to a stock case.')
return case_path
[docs]
def list_cases(rpath='.', no_print=False):
"""
List stock cases under a given folder relative to ``ams/cases``
"""
case_path = os.path.join(cases_root(), rpath)
case_path = os.path.normpath(case_path)
tree = DisplayablePath.make_tree(pathlib.Path(case_path))
out = []
for path in tree:
out.append(path.displayable())
out = '\n'.join(out)
if no_print is False:
print(out)
else:
return out
[docs]
def get_config_path(file_name='ams.rc'):
"""
Return the path of the config file to be loaded.
Search Priority: 1. current directory; 2. home directory.
Parameters
----------
file_name : str, optional
Config file name with the default as ``ams.rc``.
Returns
-------
Config path in string if found; None otherwise.
"""
conf_path = None
home_dir = os.path.expanduser('~')
# test ./ams.conf
if os.path.isfile(file_name):
conf_path = file_name
# test ~/ams.conf
elif os.path.isfile(os.path.join(home_dir, '.ams', file_name)):
conf_path = os.path.join(home_dir, '.ams', file_name)
return conf_path
[docs]
def get_pycode_path(pycode_path=None, mkdir=False):
"""
Get the path to the ``pycode`` folder.
"""
if pycode_path is None:
pycode_path = os.path.join(get_dot_ams_path(), 'pycode')
if mkdir is True:
os.makedirs(pycode_path, exist_ok=True)
return pycode_path
[docs]
def get_pkl_path():
"""
Get the path to the picked/dilled function calls.
Returns
-------
str
Path to the calls.pkl file
"""
pkl_name = 'calls.pkl'
ams_path = get_dot_ams_path()
if not os.path.exists(ams_path):
os.makedirs(ams_path)
pkl_path = os.path.join(ams_path, pkl_name)
return pkl_path
[docs]
def get_dot_ams_path():
"""
Return the path to ``$HOME/.ams``
"""
return os.path.join(str(pathlib.Path.home()), '.ams')
[docs]
def get_log_dir():
"""
Get the directory for log file.
The default is ``<tempdir>/ams``, where ``<tempdir>`` is provided by ``tempfile.gettempdir()``.
Returns
-------
str
The path to the temporary logging directory
"""
tempdir = tempfile.gettempdir()
path = tempfile.mkdtemp(prefix='ams-', dir=tempdir)
return path
[docs]
def confirm_overwrite(outfile, overwrite=None):
"""
Confirm overwriting a file.
"""
try:
if os.path.isfile(outfile):
if overwrite is None:
choice = input(f'File "{outfile}" already exist. Overwrite? [y/N]').lower()
if len(choice) == 0 or choice[0] != 'y':
logger.warning(f'File "{outfile}" not overwritten.')
return False
elif overwrite is False:
return False
except TypeError:
pass
return True
def _config_numpy(seed='None', divide='warn', invalid='warn'):
"""
Configure NumPy error handling and random seed.
Notes
-----
Adapted from ``andes.system._config_numpy``.
Original author: Hantao Cui. License: GPL-3.0.
"""
import numpy as np
if isinstance(seed, int):
np.random.seed(seed)
logger.debug("Random seed set to <%d>.", seed)
np.seterr(divide=divide, invalid=invalid)
[docs]
def load_config_rc(conf_path=None):
"""
Load config from an rc-formatted file.
Parameters
----------
conf_path : None or str
Path to the config file. If ``None``, the function returns ``None``
without reading.
Returns
-------
configparser.ConfigParser or None
Notes
-----
Adapted from ``andes.system.load_config_rc``.
Original author: Hantao Cui. License: GPL-3.0.
"""
import configparser
if conf_path is None:
return None
conf = configparser.ConfigParser()
conf.read(conf_path)
logger.info('> Loaded config from file "%s"', conf_path)
return conf
[docs]
def get_export_path(system, fname, path=None, fmt='csv'):
"""
Get the absolute export path and the derived file name for
an AMS system export.
This function is not intended to be used directly by users.
Parameters
----------
system : ams.system.System
The AMS system to export. (Mocked in example)
fname : str
The descriptive file name, e.g., 'PTDF', or 'DCOPF'.
path : str, optional
The desired output path. For a directory, the file name will be generated;
For a full file path, the base name will be used with the specified format;
For ``None``, falls back to ``system.files.output_path`` when set,
otherwise the current working directory.
fmt : str, optional
The file format to export, e.g., 'csv', 'json'. Default is 'csv'.
Returns
-------
tuple : (str, str)
- The absolute export path (e.g., '/home/user/project/data_Routine.csv').
- The export file name (e.g., 'data_Routine.csv'), including the format extension.
Notes
-----
Default-path precedence (when ``path is None``):
explicit ``path`` arg > ``system.files.output_path`` > current working
directory. An empty-string ``output_path`` (the ``FileMan`` default
when no output dir is specified) is treated as "not set".
.. versionchanged:: 1.3.0
``system.files.output_path`` is now used as the default fallback,
previously the current working directory was always used.
"""
# Determine the base name from system.files.fullname or default to "Untitled"
if system.files.fullname is None:
logger.info("Input file name not detected. Using `Untitled`.")
base_name_prefix = f'Untitled_{fname}'
else:
base_name_prefix = os.path.splitext(os.path.basename(system.files.fullname))[0]
base_name_prefix += f'_{fname}'
target_extension = fmt.lower() # Ensure consistent extension casing
if path:
abs_path = os.path.abspath(path) # Resolve to absolute path early
# Check if the provided path is likely intended as a directory
if not os.path.splitext(abs_path)[1]: # No extension implies it's a directory
dir_path = abs_path
final_file_name = f"{base_name_prefix}.{target_extension}"
full_export_path = os.path.join(dir_path, final_file_name)
else:
# Path includes a filename. Use its directory, and its base name.
dir_path = os.path.dirname(abs_path)
# Use the provided filename's base, but enforce the target_extension
provided_base_filename = os.path.splitext(os.path.basename(abs_path))[0]
final_file_name = f"{provided_base_filename}.{target_extension}"
full_export_path = os.path.join(dir_path, final_file_name)
else:
# No path provided. Prefer system.files.output_path when set, else CWD.
output_path = getattr(system.files, 'output_path', None)
dir_path = os.path.abspath(output_path) if output_path else os.getcwd()
final_file_name = f"{base_name_prefix}.{target_extension}"
full_export_path = os.path.join(dir_path, final_file_name)
return full_export_path, final_file_name