Skip to content

API Reference

block

Attribute

Represents an attribute in an SMS++ model.

Attributes store metadata and configuration parameters for blocks in the SMS++ hierarchical structure. They can hold string, integer, or floating-point values.

Attributes:

Name Type Description
name str

The name of the attribute.

value str | int | float

The value of the attribute.

Examples:

>>> attr = Attribute("block_type", "UCBlock")
>>> attr = Attribute("TimeHorizon", 24)
>>> attr = Attribute("LinearTerm", 0.3)
Source code in pysmspp/block.py
class Attribute:
    """
    Represents an attribute in an SMS++ model.

    Attributes store metadata and configuration parameters for blocks in the
    SMS++ hierarchical structure. They can hold string, integer, or floating-point
    values.

    Attributes
    ----------
    name : str
        The name of the attribute.
    value : str | int | float
        The value of the attribute.

    Examples
    --------
    >>> attr = Attribute("block_type", "UCBlock")
    >>> attr = Attribute("TimeHorizon", 24)
    >>> attr = Attribute("LinearTerm", 0.3)
    """

    name: str
    value: str | int | float

    def __init__(self, name: str, value: str | int | float):
        """
        Initialize an Attribute object.

        Parameters
        ----------
        name : str
            The name of the attribute.
        value : str | int | float
            The value of the attribute.
        """
        self.name = name
        self.value = value

    def __str__(self) -> str:
        """Return string representation of the attribute value."""
        return str(self.value)

    def __repr__(self) -> str:
        """Return detailed representation of the attribute."""
        return f"Attribute(name={self.name!r}, value={self.value!r})"

    def __eq__(self, other) -> bool:
        """Compare attribute with another value or Attribute object."""
        if isinstance(other, Attribute):
            return self.name == other.name and self.value == other.value
        return self.value == other

__eq__(other)

Compare attribute with another value or Attribute object.

Source code in pysmspp/block.py
def __eq__(self, other) -> bool:
    """Compare attribute with another value or Attribute object."""
    if isinstance(other, Attribute):
        return self.name == other.name and self.value == other.value
    return self.value == other

__init__(name, value)

Initialize an Attribute object.

Parameters:

Name Type Description Default
name str

The name of the attribute.

required
value str | int | float

The value of the attribute.

required
Source code in pysmspp/block.py
def __init__(self, name: str, value: str | int | float):
    """
    Initialize an Attribute object.

    Parameters
    ----------
    name : str
        The name of the attribute.
    value : str | int | float
        The value of the attribute.
    """
    self.name = name
    self.value = value

__repr__()

Return detailed representation of the attribute.

Source code in pysmspp/block.py
def __repr__(self) -> str:
    """Return detailed representation of the attribute."""
    return f"Attribute(name={self.name!r}, value={self.value!r})"

__str__()

Return string representation of the attribute value.

Source code in pysmspp/block.py
def __str__(self) -> str:
    """Return string representation of the attribute value."""
    return str(self.value)

Block

Hierarchical container for SMS++ model components.

A Block is the fundamental building component of SMS++ models, providing a hierarchical structure to organize attributes, dimensions, variables, and sub-blocks. Blocks can be nested to create complex optimization models with multiple layers of structure.

The Block class supports: - Reading from and writing to NetCDF4 files - Dynamic construction from attributes, dimensions, variables, and sub-blocks - Hierarchical nesting of blocks - Type-based component management

Attributes:

Name Type Description
attributes Dict

Dictionary of Attribute objects containing metadata and parameters.

dimensions Dict

Dictionary of Dimension objects defining array sizes.

variables Dict

Dictionary of Variable objects containing data arrays.

blocks Dict

Dictionary of nested Block objects forming the hierarchy.

components Dict

Configuration dictionary for component types.

Examples:

Create an empty block:

>>> block = Block()

Create a block from a NetCDF file:

>>> block = Block(fp="model.nc")

Create a block with attributes:

>>> block = Block(attributes={"type": "UCBlock"})

Create a block with variables using kwargs:

>>> block = Block(MinPower=Variable("MinPower", "float", (), 0.0))
See Also

SMSNetwork : Network-level block for complete SMS++ models Attribute : Metadata storage Dimension : Array dimension definition Variable : Data array storage

Source code in pysmspp/block.py
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
class Block:
    """
    Hierarchical container for SMS++ model components.

    A Block is the fundamental building component of SMS++ models, providing a
    hierarchical structure to organize attributes, dimensions, variables, and
    sub-blocks. Blocks can be nested to create complex optimization models with
    multiple layers of structure.

    The Block class supports:
    - Reading from and writing to NetCDF4 files
    - Dynamic construction from attributes, dimensions, variables, and sub-blocks
    - Hierarchical nesting of blocks
    - Type-based component management

    Attributes
    ----------
    attributes : Dict
        Dictionary of Attribute objects containing metadata and parameters.
    dimensions : Dict
        Dictionary of Dimension objects defining array sizes.
    variables : Dict
        Dictionary of Variable objects containing data arrays.
    blocks : Dict
        Dictionary of nested Block objects forming the hierarchy.
    components : Dict
        Configuration dictionary for component types.

    Examples
    --------
    Create an empty block:

    >>> block = Block()

    Create a block from a NetCDF file:

    >>> block = Block(fp="model.nc")

    Create a block with attributes:

    >>> block = Block(attributes={"type": "UCBlock"})

    Create a block with variables using kwargs:

    >>> block = Block(MinPower=Variable("MinPower", "float", (), 0.0))

    See Also
    --------
    SMSNetwork : Network-level block for complete SMS++ models
    Attribute : Metadata storage
    Dimension : Array dimension definition
    Variable : Data array storage
    """

    # Class variables

    _attributes: Dict  # attributes of the block
    _dimensions: Dict  # dimensions of the block
    _variables: Dict  # variables of the block
    _blocks: Dict  # blocks beloning to the block

    components: Dict  # components of the block

    # Constructor

    def __init__(
        self,
        fp: Path | str = "",
        attributes: Dict = None,
        dimensions: Dict = None,
        variables: Dict = None,
        blocks: Dict = None,
        **kwargs,
    ):
        """
        Initialize a Block object.
        A block object can be created from a NetCDF file, or, alternatively, from the given attributes, dimensions, variables, and blocks.
        Moreover, optional additional arguments can be passed to the Block constructor to override the values loaded from files or from the arguments (attributes, dimensions, variables and blocks).

        Example of possible usage:
        >>> Block()
        >>> Block(fp="file.nc")
        >>> Block(attributes={"block_type": "UCBlock"})
        >>> Block(dimensions={"n": 10})
        >>> Block(variables={"var1": Variable("var1", "float", None, 0.0)})
        >>> Block(blocks={"Block_0": Block()})
        >>> Block(fp="file.nc", attributes={"block_type": "UCBlock"})
        >>> Block(MinPower=Variable("MinPower", "float", None, 0.0))

        Parameters
        ----------
        fp : Path | str (default: "")
            The path to the NetCDF file to read.
        attributes : Dict (default: None)
            The attributes of the block.
        dimensions : Dict (default: None)
            The dimensions of the block.
        variables : Dict (default: None)
            The variables of the block.
        blocks : Dict (default: None)
            The blocks of the block.
        kwargs : dict
            The arguments to pass to the Block constructor.
        """
        self.components = Dict(components.T.to_dict())
        if fp:
            obj = self.from_netcdf(fp)
            self._attributes = obj.attributes
            self._dimensions = obj.dimensions
            self._variables = obj.variables
            self._blocks = obj.blocks
        else:
            self._attributes = attributes if attributes else Dict()
            self._dimensions = dimensions if dimensions else Dict()
            self._variables = variables if variables else Dict()
            self._blocks = blocks if blocks else Dict()
        self.from_kwargs(**kwargs)

    def __repr__(self):
        """
        Return a string representation of the Block object.

        Returns
        -------
        str
            A formatted string showing the counts and names of attributes,
            dimensions, variables, and sub-blocks.
        """
        # Extract the keys of the dictionaries
        dim_str = ", ".join(self.dimensions.keys()) if self.dimensions else "None"
        var_str = ", ".join(self.variables.keys()) if self.variables else "None"
        attr_str = ", ".join(self.attributes.keys()) if self.attributes else "None"
        block_str = ", ".join(self.blocks.keys()) if self.blocks else "None"

        return (
            f"Block object\n"
            f"Attributes ({len(self.attributes)}): {attr_str}\n"
            f"Dimensions ({len(self.dimensions)}): {dim_str}\n"
            f"Variables ({len(self.variables)}): {var_str}\n"
            f"Blocks ({len(self.blocks)}): {block_str}"
        )

    # Properties
    @property
    def attributes(self) -> Dict:
        """Return the attributes of the block."""
        return self._attributes

    @property
    def dimensions(self) -> Dict:
        """Return the dimensions of the block."""
        return self._dimensions

    @property
    def variables(self) -> Dict:
        """Return the variables of the block."""
        return self._variables

    @property
    def blocks(self) -> Dict:
        """Return the blocks of the block."""
        return self._blocks

    @property
    def block_type(self, ignore_missing: bool = True) -> str:
        """Return the type of the block."""
        if "type" in self.attributes:
            return self.attributes["type"].value
        elif ignore_missing:
            return None
        raise AttributeError("Block type not defined.")

    @block_type.setter
    def block_type(self, block_type: str):
        """
        Set the type of the block.

        Parameters
        ----------
        block_type : str
            The type of the block.
        """
        self.attributes["type"] = Attribute("type", block_type)

    def add_attribute(self, name: str, value, force: bool = False):
        """
        Add an attribute to the block.

        Parameters
        ----------
        name : str
            The name of the attribute
        value : any
            The value of the attribute. Can be provided as a plain value
            or as an Attribute object.
        force : bool (default: False)
            If True, overwrite the attribute if it exists.

        Returns
        -------
        Attribute
            Returns the Attribute object being created.
        """
        if not force and name in self.attributes:
            raise ValueError(f"Attribute {name} already exists.")
        if isinstance(value, Attribute):
            self.attributes[name] = value
        else:
            self.attributes[name] = Attribute(name, value)
        return self.attributes[name]

    def add_dimension(self, name: str, value: int, force: bool = False):
        """
        Add a dimension to the block.

        Parameters
        ----------
        name : str
            The name of the dimension
        value : int
            The value of the dimension. Can be provided as a plain value
            or as a Dimension object.
        force : bool (default: False)
            If True, overwrite the dimension if it exists.

        Returns
        -------
        Dimension
            Returns the Dimension object being created.
        """
        if not force and name in self.dimensions:
            raise ValueError(f"Dimension {name} already exists.")
        if isinstance(value, Dimension):
            self.dimensions[name] = value
        else:
            self.dimensions[name] = Dimension(name, value)
        return self.dimensions[name]

    def add_variable(
        self,
        name,
        *args,
        force: bool = False,
        **kwargs,
    ):
        """
        Add a variable to the block.

        Parameters
        ----------
        name : str
            The name of the variable
        var_type : str
            The type of the variable
        dimensions : tuple
            The dimensions of the variable
        data : float | list | np.ndarray
            The data of the variable
        force : bool (default: False)
            If True, overwrite the variable if it exists.

        Returns
        -------
        Returns the variable being created.
        """
        if not force and name in self.variables:
            raise ValueError(f"Variable {name} already exists.")
        if len(args) == 1:
            assert isinstance(args[0], Variable), "args must be a Variable object."
            self.variables[name] = args[0]
        else:
            self.variables[name] = Variable(name, *args, **kwargs)
        return self.variables[name]

    def add_block(self, name: str, *args, **kwargs):
        """
        Add a block.

        >>> add_block("Block_0", block=Block())
        >>> add_block("Block_0", Block())
        >>> add_block("Block_0", **kwargs})

        Parameters
        ----------
        name : str
            The name of the block
        args : list
            The arguments to pass to the Block constructor.
            If a Block argument is passed, it is used as the block.
        kwargs : dict
            The attributes of the block.
            If the argument "block" is present, the block is set to that value.
            Otherwise, arguments are passed to the Block constructor.

        Returns
        -------
        Returns the block being created.
        """
        force = kwargs.pop("force", False)
        if not force and name in self.blocks:
            raise ValueError(f"Block {name} already exists.")
        if "block" in kwargs:
            if not isinstance(kwargs["block"], Block):
                raise ValueError("block must be a Block object.")
            self.blocks[name] = kwargs["block"]
        elif len(args) >= 1:
            if len(args) == 1 and isinstance(args[0], Block):
                self.blocks[name] = args[0]
            else:
                raise ValueError("Non accepted arguments have been passed.")
        else:
            self.blocks[name] = Block().from_kwargs(**kwargs)
        return self.blocks[name]

    def from_kwargs(self, **kwargs):
        """
        Populate the Block from keyword arguments.

        Parameters
        ----------
        **kwargs : dict
            Keyword arguments representing block components. If 'block_type' is
            provided, it sets the block type and other arguments are added as
            components based on the block type configuration.

        Returns
        -------
        Block
            Returns self for method chaining.
        """
        if "block_type" in kwargs:
            btype = kwargs.pop("block_type")
            self.block_type = btype
        for key, value in kwargs.items():
            nc_cmp = get_attr_field(self.block_type, key, value, "smspp_object")
            self.add(nc_cmp, key, value)
        return self

    # Input/Output operations

    def _to_netcdf_helper(self, grp: nc.Dataset | nc.Group):
        """
        Recursively save Block and sub-blocks to a NetCDF group.

        Parameters
        ----------
        grp : netCDF4.Dataset or netCDF4.Group
            The NetCDF dataset or group to write to.
        """
        # Add the block's attributes
        for key, attr in self.attributes.items():
            grp.setncattr(key, attr.value)

        # Add the dimensions
        for key, dim in self.dimensions.items():
            grp.createDimension(key, dim.value)

        # Add the variables
        for key, value in self.variables.items():
            var = grp.createVariable(key, value.var_type, value.dimensions)
            var[:] = value.data

        # Save each sub-Block as a subgroup
        for key, sub_block in self.blocks.items():
            subgrp = grp.createGroup(key)
            sub_block._to_netcdf_helper(subgrp)

    def to_netcdf(self, fp: Path | str, force: bool = False):
        """
        Write the SMSNetwork object to a netCDF4 file.

        Parameters
        ----------
        fp : Path | str
            The path to the file to write.
        force : bool (default: False)
            If True, overwrite the file if it exists.
        """
        if not force and os.path.exists(fp):
            raise FileExistsError("File already exists; reading file not implemented.")

        with nc.Dataset(fp, "w") as ds:
            self._to_netcdf_helper(ds)

    @classmethod
    def _from_netcdf(cls, grb: nc.Dataset | nc.Group):
        """
        Recursively load Block and sub-blocks from a NetCDF group.

        Parameters
        ----------
        grb : netCDF4.Dataset or netCDF4.Group
            The NetCDF dataset or group to read from.

        Returns
        -------
        Block
            A new Block instance populated with data from the NetCDF group.
        """
        # Create a new block
        new_block = cls()

        # Retrieve attributes
        for name in grb.ncattrs():
            new_block.add_attribute(name, grb.getncattr(name), force=True)

        # Retrieve dimensions
        for dimname, dimobj in grb.dimensions.items():
            new_block.add_dimension(dimname, dimobj.size)

        # Retrieve variables
        for varname, varobj in grb.variables.items():
            new_block.add_variable(
                varname,
                var_type=varobj.dtype,
                dimensions=varobj.dimensions,
                data=varobj[:],
            )

        # Recursively load sub-blocks
        for subgrp_name, subgrb in grb.groups.items():
            new_block.add_block(subgrp_name, block=Block._from_netcdf(subgrb))

        return new_block

    @classmethod
    def from_netcdf(cls, filename):
        """
        Deserialize a NetCDF file to create a Block instance.

        Parameters
        ----------
        filename : str or Path
            Path to the NetCDF file to read.

        Returns
        -------
        Block
            A new Block instance with nested sub-blocks from the file.
        """
        with nc.Dataset(filename, "r") as ncfile:
            return cls._from_netcdf(ncfile)

    # Functions

    def add(self, component_name, name, *args, **kwargs):
        """
        Add a component to the block.

        Dispatches to the appropriate add method based on component type.

        Parameters
        ----------
        component_name : str
            The SMS++ component class name (e.g., 'Attribute', 'Dimension',
            'Variable', or a Block type).
        name : str
            The name of the component to add.
        *args : tuple
            Positional arguments passed to the specific add method.
        **kwargs : dict
            Keyword arguments passed to the specific add method.

        Returns
        -------
        Attribute, Dimension, Variable, or Block
            The created component object.
        """
        component_nctype = self.components[component_name]["nctype"]
        if component_nctype == "Attribute":
            return self.add_attribute(name, *args, **kwargs)
        elif component_nctype == "Dimension":
            return self.add_dimension(name, *args, **kwargs)
        elif component_nctype == "Variable":
            return self.add_variable(name, *args, **kwargs)
        elif component_nctype == "Block":
            return self.add_block(name, *args, block_type=component_name, **kwargs)
        else:
            raise ValueError(f"Class {component_name} not supported.")

    # Utilities

    def remove(self, component_name: str, name: str):
        """
        Remove a component from the block.

        Parameters
        ----------
        component_name : str
            The SMS++ component class name.
        name : str
            The name of the component to remove.

        Returns
        -------
        Attribute, Dimension, Variable, or Block
            The removed component object.
        """
        return self.static(component_name).pop(name)

    def static(self, component_name: str) -> Dict:
        """
        Return the Dictionary of static components for component_name.
        For example, for component_name = "attribute", the Dictionary of attributes is returned.

        Parameters
        ----------
        component_name : string

        Returns
        -------
        Dict

        """
        return getattr(self, self.components[component_name]["list_name"])

    def print_tree(
        self,
        name: str = None,
        show_dimensions: bool = False,
        show_variables: bool = False,
        show_attributes: bool = False,
        _indent: str = "",
        _is_last: bool = True,
        _is_root: bool = True,
    ) -> None:
        """
        Print a tree representation of the block structure.

        This method displays the hierarchical structure of blocks in a tree format,
        with optional display of dimensions, variables, and attributes.

        Parameters
        ----------
        name : str, optional
            The name of the block. If not provided, uses the block_type if available,
            otherwise defaults to "Block".
        show_dimensions : bool, optional
            Whether to display dimensions (default: False).
        show_variables : bool, optional
            Whether to display variables (default: False).
        show_attributes : bool, optional
            Whether to display attributes (default: False).
        _indent : str, optional
            Internal parameter for indentation (default: "").
        _is_last : bool, optional
            Internal parameter to track if this is the last child (default: True).
        _is_root : bool, optional
            Internal parameter to track if this is the root node (default: True).

        Examples
        --------
        >>> from pysmspp import Block
        >>> block = Block(fp="network.nc4")
        >>> block.print_tree()  # Uses block_type as name
        UCBlock [UCBlock]
        └── Block_0 [UCBlock]
            ├── UnitBlock_0 [ThermalUnitBlock]
            └── UnitBlock_1 [BatteryUnitBlock]

        >>> block.print_tree("MyNetwork")  # Uses custom name
        MyNetwork [UCBlock]
        └── Block_0 [UCBlock]
            ...

        >>> block.print_tree(show_dimensions=True, show_variables=True)
        UCBlock [UCBlock]
          Dimensions (2): n=10, m=5
          Variables (3): var1, var2, var3
        └── Block_0 [UCBlock]
            ...
        """
        # Determine the name to use
        if name is None:
            # Use block_type if available, otherwise default to "Block"
            if hasattr(self, "block_type") and self.block_type:
                name = self.block_type
            else:
                name = "Block"

        # Get block type - if it's None or missing, we'll omit the brackets
        block_type = None
        if hasattr(self, "block_type") and self.block_type:
            block_type = self.block_type

        # Print the current block
        if _is_root:
            # Root level - no connector
            if block_type:
                print(f"{name} [{block_type}]")
            else:
                print(f"{name}")
            child_indent = ""
        else:
            connector = "└── " if _is_last else "├── "
            if block_type:
                print(f"{_indent}{connector}{name} [{block_type}]")
            else:
                print(f"{_indent}{connector}{name}")
            child_indent = _indent + ("    " if _is_last else "│   ")

        # Print dimensions if requested
        if show_dimensions and self.dimensions:
            dims_str = ", ".join(
                f"{key}={value}" for key, value in self.dimensions.items()
            )
            detail_indent = child_indent if not _is_root else "  "
            print(f"{detail_indent}Dimensions ({len(self.dimensions)}): {dims_str}")

        # Print variables if requested
        if show_variables and self.variables:
            vars_list = list(self.variables.keys())
            if len(vars_list) <= 5:
                vars_str = ", ".join(vars_list)
            else:
                vars_str = ", ".join(vars_list[:5]) + f", ... ({len(vars_list)} total)"
            detail_indent = child_indent if not _is_root else "  "
            print(f"{detail_indent}Variables ({len(self.variables)}): {vars_str}")

        # Print attributes if requested (exclude 'type' since it's shown in brackets)
        if show_attributes and self.attributes:
            attrs = {k: v for k, v in self.attributes.items() if k != "type"}
            if attrs:
                attrs_list = [f"{k}={v}" for k, v in list(attrs.items())[:5]]
                if len(attrs) <= 5:
                    attrs_str = ", ".join(attrs_list)
                else:
                    attrs_str = ", ".join(attrs_list) + f", ... ({len(attrs)} total)"
                detail_indent = child_indent if not _is_root else "  "
                print(f"{detail_indent}Attributes ({len(attrs)}): {attrs_str}")

        # Recursively print sub-blocks
        if self.blocks:
            sub_blocks = list(self.blocks.items())
            for i, (sub_name, sub_block) in enumerate(sub_blocks):
                is_last_child = i == len(sub_blocks) - 1
                sub_block.print_tree(
                    sub_name,
                    show_dimensions,
                    show_variables,
                    show_attributes,
                    child_indent,
                    is_last_child,
                    False,
                )

    def plot(self, variables: list = None, figsize: tuple = None, **kwargs):
        """
        Plot variables of the block.

        Each variable is rendered in its own subplot using :meth:`Variable.plot`.
        Only variables whose data array has at least 1 dimension are plotted by
        default; scalar variables are included when they are explicitly listed in
        *variables*.

        Parameters
        ----------
        variables : list of str, optional
            Names of the variables to plot. If *None*, all variables whose data
            is a 1-D or 2-D array are included.
        figsize : tuple of (float, float), optional
            Figure size ``(width, height)`` in inches passed to
            ``matplotlib.pyplot.subplots``. If *None* each subplot is given
            a height of 3 inches and a width of 8 inches.
        **kwargs : dict
            Additional keyword arguments forwarded to :meth:`Variable.plot`
            for each subplot (e.g. ``kind="line"``).

        Returns
        -------
        matplotlib.figure.Figure
            The figure containing the subplots.

        Raises
        ------
        ValueError
            If no plottable variables are found.

        Examples
        --------
        >>> block = Block(fp="model.nc")
        >>> fig = block.plot()

        >>> fig = block.plot(variables=["ActivePowerDemand", "MaxPowerFlow"])

        >>> fig = block.plot(kind="line")
        """
        import matplotlib.pyplot as plt

        if variables is None:
            vars_to_plot = {
                name: var
                for name, var in self.variables.items()
                if np.asarray(var.data).ndim >= 1
            }
        else:
            vars_to_plot = {name: self.variables[name] for name in variables}

        if not vars_to_plot:
            raise ValueError(
                "No plottable variables found in the block. "
                "Use 'variables' to explicitly specify variable names."
            )

        n = len(vars_to_plot)
        if figsize is None:
            figsize = (8, 3 * n)
        fig, axes = plt.subplots(n, 1, figsize=figsize, squeeze=False)

        for ax, (name, var) in zip(axes[:, 0], vars_to_plot.items()):
            var.plot(ax=ax, **kwargs)

        fig.tight_layout()
        return fig

attributes property

Return the attributes of the block.

block_type property writable

Return the type of the block.

blocks property

Return the blocks of the block.

dimensions property

Return the dimensions of the block.

variables property

Return the variables of the block.

__init__(fp='', attributes=None, dimensions=None, variables=None, blocks=None, **kwargs)

Initialize a Block object. A block object can be created from a NetCDF file, or, alternatively, from the given attributes, dimensions, variables, and blocks. Moreover, optional additional arguments can be passed to the Block constructor to override the values loaded from files or from the arguments (attributes, dimensions, variables and blocks).

Example of possible usage:

Block() Block(fp="file.nc") Block(attributes={"block_type": "UCBlock"}) Block(dimensions={"n": 10}) Block(variables={"var1": Variable("var1", "float", None, 0.0)}) Block(blocks={"Block_0": Block()}) Block(fp="file.nc", attributes={"block_type": "UCBlock"}) Block(MinPower=Variable("MinPower", "float", None, 0.0))

Parameters:

Name Type Description Default
fp Path | str (default: "")

The path to the NetCDF file to read.

''
attributes Dict (default: None)

The attributes of the block.

None
dimensions Dict (default: None)

The dimensions of the block.

None
variables Dict (default: None)

The variables of the block.

None
blocks Dict (default: None)

The blocks of the block.

None
kwargs dict

The arguments to pass to the Block constructor.

{}
Source code in pysmspp/block.py
def __init__(
    self,
    fp: Path | str = "",
    attributes: Dict = None,
    dimensions: Dict = None,
    variables: Dict = None,
    blocks: Dict = None,
    **kwargs,
):
    """
    Initialize a Block object.
    A block object can be created from a NetCDF file, or, alternatively, from the given attributes, dimensions, variables, and blocks.
    Moreover, optional additional arguments can be passed to the Block constructor to override the values loaded from files or from the arguments (attributes, dimensions, variables and blocks).

    Example of possible usage:
    >>> Block()
    >>> Block(fp="file.nc")
    >>> Block(attributes={"block_type": "UCBlock"})
    >>> Block(dimensions={"n": 10})
    >>> Block(variables={"var1": Variable("var1", "float", None, 0.0)})
    >>> Block(blocks={"Block_0": Block()})
    >>> Block(fp="file.nc", attributes={"block_type": "UCBlock"})
    >>> Block(MinPower=Variable("MinPower", "float", None, 0.0))

    Parameters
    ----------
    fp : Path | str (default: "")
        The path to the NetCDF file to read.
    attributes : Dict (default: None)
        The attributes of the block.
    dimensions : Dict (default: None)
        The dimensions of the block.
    variables : Dict (default: None)
        The variables of the block.
    blocks : Dict (default: None)
        The blocks of the block.
    kwargs : dict
        The arguments to pass to the Block constructor.
    """
    self.components = Dict(components.T.to_dict())
    if fp:
        obj = self.from_netcdf(fp)
        self._attributes = obj.attributes
        self._dimensions = obj.dimensions
        self._variables = obj.variables
        self._blocks = obj.blocks
    else:
        self._attributes = attributes if attributes else Dict()
        self._dimensions = dimensions if dimensions else Dict()
        self._variables = variables if variables else Dict()
        self._blocks = blocks if blocks else Dict()
    self.from_kwargs(**kwargs)

__repr__()

Return a string representation of the Block object.

Returns:

Type Description
str

A formatted string showing the counts and names of attributes, dimensions, variables, and sub-blocks.

Source code in pysmspp/block.py
def __repr__(self):
    """
    Return a string representation of the Block object.

    Returns
    -------
    str
        A formatted string showing the counts and names of attributes,
        dimensions, variables, and sub-blocks.
    """
    # Extract the keys of the dictionaries
    dim_str = ", ".join(self.dimensions.keys()) if self.dimensions else "None"
    var_str = ", ".join(self.variables.keys()) if self.variables else "None"
    attr_str = ", ".join(self.attributes.keys()) if self.attributes else "None"
    block_str = ", ".join(self.blocks.keys()) if self.blocks else "None"

    return (
        f"Block object\n"
        f"Attributes ({len(self.attributes)}): {attr_str}\n"
        f"Dimensions ({len(self.dimensions)}): {dim_str}\n"
        f"Variables ({len(self.variables)}): {var_str}\n"
        f"Blocks ({len(self.blocks)}): {block_str}"
    )

add(component_name, name, *args, **kwargs)

Add a component to the block.

Dispatches to the appropriate add method based on component type.

Parameters:

Name Type Description Default
component_name str

The SMS++ component class name (e.g., 'Attribute', 'Dimension', 'Variable', or a Block type).

required
name str

The name of the component to add.

required
*args tuple

Positional arguments passed to the specific add method.

()
**kwargs dict

Keyword arguments passed to the specific add method.

{}

Returns:

Type Description
Attribute, Dimension, Variable, or Block

The created component object.

Source code in pysmspp/block.py
def add(self, component_name, name, *args, **kwargs):
    """
    Add a component to the block.

    Dispatches to the appropriate add method based on component type.

    Parameters
    ----------
    component_name : str
        The SMS++ component class name (e.g., 'Attribute', 'Dimension',
        'Variable', or a Block type).
    name : str
        The name of the component to add.
    *args : tuple
        Positional arguments passed to the specific add method.
    **kwargs : dict
        Keyword arguments passed to the specific add method.

    Returns
    -------
    Attribute, Dimension, Variable, or Block
        The created component object.
    """
    component_nctype = self.components[component_name]["nctype"]
    if component_nctype == "Attribute":
        return self.add_attribute(name, *args, **kwargs)
    elif component_nctype == "Dimension":
        return self.add_dimension(name, *args, **kwargs)
    elif component_nctype == "Variable":
        return self.add_variable(name, *args, **kwargs)
    elif component_nctype == "Block":
        return self.add_block(name, *args, block_type=component_name, **kwargs)
    else:
        raise ValueError(f"Class {component_name} not supported.")

add_attribute(name, value, force=False)

Add an attribute to the block.

Parameters:

Name Type Description Default
name str

The name of the attribute

required
value any

The value of the attribute. Can be provided as a plain value or as an Attribute object.

required
force bool (default: False)

If True, overwrite the attribute if it exists.

False

Returns:

Type Description
Attribute

Returns the Attribute object being created.

Source code in pysmspp/block.py
def add_attribute(self, name: str, value, force: bool = False):
    """
    Add an attribute to the block.

    Parameters
    ----------
    name : str
        The name of the attribute
    value : any
        The value of the attribute. Can be provided as a plain value
        or as an Attribute object.
    force : bool (default: False)
        If True, overwrite the attribute if it exists.

    Returns
    -------
    Attribute
        Returns the Attribute object being created.
    """
    if not force and name in self.attributes:
        raise ValueError(f"Attribute {name} already exists.")
    if isinstance(value, Attribute):
        self.attributes[name] = value
    else:
        self.attributes[name] = Attribute(name, value)
    return self.attributes[name]

add_block(name, *args, **kwargs)

Add a block.

add_block("Block_0", block=Block()) add_block("Block_0", Block()) add_block("Block_0", **kwargs})

Parameters:

Name Type Description Default
name str

The name of the block

required
args list

The arguments to pass to the Block constructor. If a Block argument is passed, it is used as the block.

()
kwargs dict

The attributes of the block. If the argument "block" is present, the block is set to that value. Otherwise, arguments are passed to the Block constructor.

{}

Returns:

Type Description
Returns the block being created.
Source code in pysmspp/block.py
def add_block(self, name: str, *args, **kwargs):
    """
    Add a block.

    >>> add_block("Block_0", block=Block())
    >>> add_block("Block_0", Block())
    >>> add_block("Block_0", **kwargs})

    Parameters
    ----------
    name : str
        The name of the block
    args : list
        The arguments to pass to the Block constructor.
        If a Block argument is passed, it is used as the block.
    kwargs : dict
        The attributes of the block.
        If the argument "block" is present, the block is set to that value.
        Otherwise, arguments are passed to the Block constructor.

    Returns
    -------
    Returns the block being created.
    """
    force = kwargs.pop("force", False)
    if not force and name in self.blocks:
        raise ValueError(f"Block {name} already exists.")
    if "block" in kwargs:
        if not isinstance(kwargs["block"], Block):
            raise ValueError("block must be a Block object.")
        self.blocks[name] = kwargs["block"]
    elif len(args) >= 1:
        if len(args) == 1 and isinstance(args[0], Block):
            self.blocks[name] = args[0]
        else:
            raise ValueError("Non accepted arguments have been passed.")
    else:
        self.blocks[name] = Block().from_kwargs(**kwargs)
    return self.blocks[name]

add_dimension(name, value, force=False)

Add a dimension to the block.

Parameters:

Name Type Description Default
name str

The name of the dimension

required
value int

The value of the dimension. Can be provided as a plain value or as a Dimension object.

required
force bool (default: False)

If True, overwrite the dimension if it exists.

False

Returns:

Type Description
Dimension

Returns the Dimension object being created.

Source code in pysmspp/block.py
def add_dimension(self, name: str, value: int, force: bool = False):
    """
    Add a dimension to the block.

    Parameters
    ----------
    name : str
        The name of the dimension
    value : int
        The value of the dimension. Can be provided as a plain value
        or as a Dimension object.
    force : bool (default: False)
        If True, overwrite the dimension if it exists.

    Returns
    -------
    Dimension
        Returns the Dimension object being created.
    """
    if not force and name in self.dimensions:
        raise ValueError(f"Dimension {name} already exists.")
    if isinstance(value, Dimension):
        self.dimensions[name] = value
    else:
        self.dimensions[name] = Dimension(name, value)
    return self.dimensions[name]

add_variable(name, *args, force=False, **kwargs)

Add a variable to the block.

Parameters:

Name Type Description Default
name str

The name of the variable

required
var_type str

The type of the variable

required
dimensions tuple

The dimensions of the variable

required
data float | list | ndarray

The data of the variable

required
force bool (default: False)

If True, overwrite the variable if it exists.

False

Returns:

Type Description
Returns the variable being created.
Source code in pysmspp/block.py
def add_variable(
    self,
    name,
    *args,
    force: bool = False,
    **kwargs,
):
    """
    Add a variable to the block.

    Parameters
    ----------
    name : str
        The name of the variable
    var_type : str
        The type of the variable
    dimensions : tuple
        The dimensions of the variable
    data : float | list | np.ndarray
        The data of the variable
    force : bool (default: False)
        If True, overwrite the variable if it exists.

    Returns
    -------
    Returns the variable being created.
    """
    if not force and name in self.variables:
        raise ValueError(f"Variable {name} already exists.")
    if len(args) == 1:
        assert isinstance(args[0], Variable), "args must be a Variable object."
        self.variables[name] = args[0]
    else:
        self.variables[name] = Variable(name, *args, **kwargs)
    return self.variables[name]

from_kwargs(**kwargs)

Populate the Block from keyword arguments.

Parameters:

Name Type Description Default
**kwargs dict

Keyword arguments representing block components. If 'block_type' is provided, it sets the block type and other arguments are added as components based on the block type configuration.

{}

Returns:

Type Description
Block

Returns self for method chaining.

Source code in pysmspp/block.py
def from_kwargs(self, **kwargs):
    """
    Populate the Block from keyword arguments.

    Parameters
    ----------
    **kwargs : dict
        Keyword arguments representing block components. If 'block_type' is
        provided, it sets the block type and other arguments are added as
        components based on the block type configuration.

    Returns
    -------
    Block
        Returns self for method chaining.
    """
    if "block_type" in kwargs:
        btype = kwargs.pop("block_type")
        self.block_type = btype
    for key, value in kwargs.items():
        nc_cmp = get_attr_field(self.block_type, key, value, "smspp_object")
        self.add(nc_cmp, key, value)
    return self

from_netcdf(filename) classmethod

Deserialize a NetCDF file to create a Block instance.

Parameters:

Name Type Description Default
filename str or Path

Path to the NetCDF file to read.

required

Returns:

Type Description
Block

A new Block instance with nested sub-blocks from the file.

Source code in pysmspp/block.py
@classmethod
def from_netcdf(cls, filename):
    """
    Deserialize a NetCDF file to create a Block instance.

    Parameters
    ----------
    filename : str or Path
        Path to the NetCDF file to read.

    Returns
    -------
    Block
        A new Block instance with nested sub-blocks from the file.
    """
    with nc.Dataset(filename, "r") as ncfile:
        return cls._from_netcdf(ncfile)

plot(variables=None, figsize=None, **kwargs)

Plot variables of the block.

Each variable is rendered in its own subplot using :meth:Variable.plot. Only variables whose data array has at least 1 dimension are plotted by default; scalar variables are included when they are explicitly listed in variables.

Parameters:

Name Type Description Default
variables list of str

Names of the variables to plot. If None, all variables whose data is a 1-D or 2-D array are included.

None
figsize tuple of (float, float)

Figure size (width, height) in inches passed to matplotlib.pyplot.subplots. If None each subplot is given a height of 3 inches and a width of 8 inches.

None
**kwargs dict

Additional keyword arguments forwarded to :meth:Variable.plot for each subplot (e.g. kind="line").

{}

Returns:

Type Description
Figure

The figure containing the subplots.

Raises:

Type Description
ValueError

If no plottable variables are found.

Examples:

>>> block = Block(fp="model.nc")
>>> fig = block.plot()
>>> fig = block.plot(variables=["ActivePowerDemand", "MaxPowerFlow"])
>>> fig = block.plot(kind="line")
Source code in pysmspp/block.py
def plot(self, variables: list = None, figsize: tuple = None, **kwargs):
    """
    Plot variables of the block.

    Each variable is rendered in its own subplot using :meth:`Variable.plot`.
    Only variables whose data array has at least 1 dimension are plotted by
    default; scalar variables are included when they are explicitly listed in
    *variables*.

    Parameters
    ----------
    variables : list of str, optional
        Names of the variables to plot. If *None*, all variables whose data
        is a 1-D or 2-D array are included.
    figsize : tuple of (float, float), optional
        Figure size ``(width, height)`` in inches passed to
        ``matplotlib.pyplot.subplots``. If *None* each subplot is given
        a height of 3 inches and a width of 8 inches.
    **kwargs : dict
        Additional keyword arguments forwarded to :meth:`Variable.plot`
        for each subplot (e.g. ``kind="line"``).

    Returns
    -------
    matplotlib.figure.Figure
        The figure containing the subplots.

    Raises
    ------
    ValueError
        If no plottable variables are found.

    Examples
    --------
    >>> block = Block(fp="model.nc")
    >>> fig = block.plot()

    >>> fig = block.plot(variables=["ActivePowerDemand", "MaxPowerFlow"])

    >>> fig = block.plot(kind="line")
    """
    import matplotlib.pyplot as plt

    if variables is None:
        vars_to_plot = {
            name: var
            for name, var in self.variables.items()
            if np.asarray(var.data).ndim >= 1
        }
    else:
        vars_to_plot = {name: self.variables[name] for name in variables}

    if not vars_to_plot:
        raise ValueError(
            "No plottable variables found in the block. "
            "Use 'variables' to explicitly specify variable names."
        )

    n = len(vars_to_plot)
    if figsize is None:
        figsize = (8, 3 * n)
    fig, axes = plt.subplots(n, 1, figsize=figsize, squeeze=False)

    for ax, (name, var) in zip(axes[:, 0], vars_to_plot.items()):
        var.plot(ax=ax, **kwargs)

    fig.tight_layout()
    return fig

print_tree(name=None, show_dimensions=False, show_variables=False, show_attributes=False, _indent='', _is_last=True, _is_root=True)

Print a tree representation of the block structure.

This method displays the hierarchical structure of blocks in a tree format, with optional display of dimensions, variables, and attributes.

Parameters:

Name Type Description Default
name str

The name of the block. If not provided, uses the block_type if available, otherwise defaults to "Block".

None
show_dimensions bool

Whether to display dimensions (default: False).

False
show_variables bool

Whether to display variables (default: False).

False
show_attributes bool

Whether to display attributes (default: False).

False
_indent str

Internal parameter for indentation (default: "").

''
_is_last bool

Internal parameter to track if this is the last child (default: True).

True
_is_root bool

Internal parameter to track if this is the root node (default: True).

True

Examples:

>>> from pysmspp import Block
>>> block = Block(fp="network.nc4")
>>> block.print_tree()  # Uses block_type as name
UCBlock [UCBlock]
└── Block_0 [UCBlock]
    ├── UnitBlock_0 [ThermalUnitBlock]
    └── UnitBlock_1 [BatteryUnitBlock]
>>> block.print_tree("MyNetwork")  # Uses custom name
MyNetwork [UCBlock]
└── Block_0 [UCBlock]
    ...
>>> block.print_tree(show_dimensions=True, show_variables=True)
UCBlock [UCBlock]
  Dimensions (2): n=10, m=5
  Variables (3): var1, var2, var3
└── Block_0 [UCBlock]
    ...
Source code in pysmspp/block.py
def print_tree(
    self,
    name: str = None,
    show_dimensions: bool = False,
    show_variables: bool = False,
    show_attributes: bool = False,
    _indent: str = "",
    _is_last: bool = True,
    _is_root: bool = True,
) -> None:
    """
    Print a tree representation of the block structure.

    This method displays the hierarchical structure of blocks in a tree format,
    with optional display of dimensions, variables, and attributes.

    Parameters
    ----------
    name : str, optional
        The name of the block. If not provided, uses the block_type if available,
        otherwise defaults to "Block".
    show_dimensions : bool, optional
        Whether to display dimensions (default: False).
    show_variables : bool, optional
        Whether to display variables (default: False).
    show_attributes : bool, optional
        Whether to display attributes (default: False).
    _indent : str, optional
        Internal parameter for indentation (default: "").
    _is_last : bool, optional
        Internal parameter to track if this is the last child (default: True).
    _is_root : bool, optional
        Internal parameter to track if this is the root node (default: True).

    Examples
    --------
    >>> from pysmspp import Block
    >>> block = Block(fp="network.nc4")
    >>> block.print_tree()  # Uses block_type as name
    UCBlock [UCBlock]
    └── Block_0 [UCBlock]
        ├── UnitBlock_0 [ThermalUnitBlock]
        └── UnitBlock_1 [BatteryUnitBlock]

    >>> block.print_tree("MyNetwork")  # Uses custom name
    MyNetwork [UCBlock]
    └── Block_0 [UCBlock]
        ...

    >>> block.print_tree(show_dimensions=True, show_variables=True)
    UCBlock [UCBlock]
      Dimensions (2): n=10, m=5
      Variables (3): var1, var2, var3
    └── Block_0 [UCBlock]
        ...
    """
    # Determine the name to use
    if name is None:
        # Use block_type if available, otherwise default to "Block"
        if hasattr(self, "block_type") and self.block_type:
            name = self.block_type
        else:
            name = "Block"

    # Get block type - if it's None or missing, we'll omit the brackets
    block_type = None
    if hasattr(self, "block_type") and self.block_type:
        block_type = self.block_type

    # Print the current block
    if _is_root:
        # Root level - no connector
        if block_type:
            print(f"{name} [{block_type}]")
        else:
            print(f"{name}")
        child_indent = ""
    else:
        connector = "└── " if _is_last else "├── "
        if block_type:
            print(f"{_indent}{connector}{name} [{block_type}]")
        else:
            print(f"{_indent}{connector}{name}")
        child_indent = _indent + ("    " if _is_last else "│   ")

    # Print dimensions if requested
    if show_dimensions and self.dimensions:
        dims_str = ", ".join(
            f"{key}={value}" for key, value in self.dimensions.items()
        )
        detail_indent = child_indent if not _is_root else "  "
        print(f"{detail_indent}Dimensions ({len(self.dimensions)}): {dims_str}")

    # Print variables if requested
    if show_variables and self.variables:
        vars_list = list(self.variables.keys())
        if len(vars_list) <= 5:
            vars_str = ", ".join(vars_list)
        else:
            vars_str = ", ".join(vars_list[:5]) + f", ... ({len(vars_list)} total)"
        detail_indent = child_indent if not _is_root else "  "
        print(f"{detail_indent}Variables ({len(self.variables)}): {vars_str}")

    # Print attributes if requested (exclude 'type' since it's shown in brackets)
    if show_attributes and self.attributes:
        attrs = {k: v for k, v in self.attributes.items() if k != "type"}
        if attrs:
            attrs_list = [f"{k}={v}" for k, v in list(attrs.items())[:5]]
            if len(attrs) <= 5:
                attrs_str = ", ".join(attrs_list)
            else:
                attrs_str = ", ".join(attrs_list) + f", ... ({len(attrs)} total)"
            detail_indent = child_indent if not _is_root else "  "
            print(f"{detail_indent}Attributes ({len(attrs)}): {attrs_str}")

    # Recursively print sub-blocks
    if self.blocks:
        sub_blocks = list(self.blocks.items())
        for i, (sub_name, sub_block) in enumerate(sub_blocks):
            is_last_child = i == len(sub_blocks) - 1
            sub_block.print_tree(
                sub_name,
                show_dimensions,
                show_variables,
                show_attributes,
                child_indent,
                is_last_child,
                False,
            )

remove(component_name, name)

Remove a component from the block.

Parameters:

Name Type Description Default
component_name str

The SMS++ component class name.

required
name str

The name of the component to remove.

required

Returns:

Type Description
Attribute, Dimension, Variable, or Block

The removed component object.

Source code in pysmspp/block.py
def remove(self, component_name: str, name: str):
    """
    Remove a component from the block.

    Parameters
    ----------
    component_name : str
        The SMS++ component class name.
    name : str
        The name of the component to remove.

    Returns
    -------
    Attribute, Dimension, Variable, or Block
        The removed component object.
    """
    return self.static(component_name).pop(name)

static(component_name)

Return the Dictionary of static components for component_name. For example, for component_name = "attribute", the Dictionary of attributes is returned.

Parameters:

Name Type Description Default
component_name string
required

Returns:

Type Description
Dict
Source code in pysmspp/block.py
def static(self, component_name: str) -> Dict:
    """
    Return the Dictionary of static components for component_name.
    For example, for component_name = "attribute", the Dictionary of attributes is returned.

    Parameters
    ----------
    component_name : string

    Returns
    -------
    Dict

    """
    return getattr(self, self.components[component_name]["list_name"])

to_netcdf(fp, force=False)

Write the SMSNetwork object to a netCDF4 file.

Parameters:

Name Type Description Default
fp Path | str

The path to the file to write.

required
force bool (default: False)

If True, overwrite the file if it exists.

False
Source code in pysmspp/block.py
def to_netcdf(self, fp: Path | str, force: bool = False):
    """
    Write the SMSNetwork object to a netCDF4 file.

    Parameters
    ----------
    fp : Path | str
        The path to the file to write.
    force : bool (default: False)
        If True, overwrite the file if it exists.
    """
    if not force and os.path.exists(fp):
        raise FileExistsError("File already exists; reading file not implemented.")

    with nc.Dataset(fp, "w") as ds:
        self._to_netcdf_helper(ds)

Dimension

Represents a dimension in an SMS++ model.

Dimensions define the size of arrays and variables in the SMS++ model structure. They are used to specify the shape of multi-dimensional variables and data arrays.

Attributes:

Name Type Description
name str

The name of the dimension.

value int

The size of the dimension (number of elements).

Examples:

>>> dim = Dimension("TimeHorizon", 24)
>>> dim = Dimension("NumberNodes", 2)
Source code in pysmspp/block.py
class Dimension:
    """
    Represents a dimension in an SMS++ model.

    Dimensions define the size of arrays and variables in the SMS++ model structure.
    They are used to specify the shape of multi-dimensional variables and data arrays.

    Attributes
    ----------
    name : str
        The name of the dimension.
    value : int
        The size of the dimension (number of elements).

    Examples
    --------
    >>> dim = Dimension("TimeHorizon", 24)
    >>> dim = Dimension("NumberNodes", 2)
    """

    name: str
    value: int

    def __init__(self, name: str, value: int):
        """
        Initialize a Dimension object.

        Parameters
        ----------
        name : str
            The name of the dimension.
        value : int
            The size of the dimension.
        """
        self.name = name
        self.value = value

    def __str__(self) -> str:
        """Return string representation of the dimension value."""
        return str(self.value)

    def __repr__(self) -> str:
        """Return detailed representation of the dimension."""
        return f"Dimension(name={self.name!r}, value={self.value!r})"

    def __eq__(self, other) -> bool:
        """Compare dimension with another value or Dimension object."""
        if isinstance(other, Dimension):
            return self.name == other.name and self.value == other.value
        return self.value == other

__eq__(other)

Compare dimension with another value or Dimension object.

Source code in pysmspp/block.py
def __eq__(self, other) -> bool:
    """Compare dimension with another value or Dimension object."""
    if isinstance(other, Dimension):
        return self.name == other.name and self.value == other.value
    return self.value == other

__init__(name, value)

Initialize a Dimension object.

Parameters:

Name Type Description Default
name str

The name of the dimension.

required
value int

The size of the dimension.

required
Source code in pysmspp/block.py
def __init__(self, name: str, value: int):
    """
    Initialize a Dimension object.

    Parameters
    ----------
    name : str
        The name of the dimension.
    value : int
        The size of the dimension.
    """
    self.name = name
    self.value = value

__repr__()

Return detailed representation of the dimension.

Source code in pysmspp/block.py
def __repr__(self) -> str:
    """Return detailed representation of the dimension."""
    return f"Dimension(name={self.name!r}, value={self.value!r})"

__str__()

Return string representation of the dimension value.

Source code in pysmspp/block.py
def __str__(self) -> str:
    """Return string representation of the dimension value."""
    return str(self.value)

SMSConfig

Configuration manager for SMS++ solver settings.

SMSConfig manages solver configuration files for SMS++ optimization. It can load configurations from file paths or use predefined templates stored in the package data directory.

Configuration files specify solver parameters such as tolerances, iteration limits, decomposition strategies, and other optimization settings required by SMS++ solvers.

Attributes:

Name Type Description
config str

The absolute path to the configuration file.

Examples:

Load from a template:

>>> config = SMSConfig(template="UCBlock/uc_solverconfig")

Load from a file path:

>>> config = SMSConfig(fp="/path/to/config.txt")

Get available templates:

>>> templates = SMSConfig.get_templates()
See Also

SMSNetwork.optimize : Uses SMSConfig for optimization

Source code in pysmspp/block.py
class SMSConfig:
    """
    Configuration manager for SMS++ solver settings.

    SMSConfig manages solver configuration files for SMS++ optimization. It can
    load configurations from file paths or use predefined templates stored in the
    package data directory.

    Configuration files specify solver parameters such as tolerances, iteration
    limits, decomposition strategies, and other optimization settings required by
    SMS++ solvers.

    Attributes
    ----------
    config : str
        The absolute path to the configuration file.

    Examples
    --------
    Load from a template:

    >>> config = SMSConfig(template="UCBlock/uc_solverconfig")

    Load from a file path:

    >>> config = SMSConfig(fp="/path/to/config.txt")

    Get available templates:

    >>> templates = SMSConfig.get_templates()

    See Also
    --------
    SMSNetwork.optimize : Uses SMSConfig for optimization
    """

    def __init__(self, fp: Path | str = None, template: str = None):
        """
        Initialize a SMSConfig object.
        If an existing fp is provided, it is used as the configuration file; an error is thrown if the file does not exist.
        If a template is provided, the configuration file is set to the template file in the data/configs directory.
        fp and template cannot be both None.

        Parameters
        ----------
        fp : Path | str (default: None)
            The path to the configuration file.
        template : str (default: None)
            The template name of the configuration file.
        """
        if fp is None and template is None:
            raise ValueError("Either fp or template must be provided.")
        if fp is not None and template is not None:
            raise ValueError("fp or template cannot be specified together.")

        if template is None:
            fp_p = Path(fp)
            if not fp_p.exists():
                raise FileNotFoundError(f"File {fp} not found.")
            else:
                self._config = str(fp_p.resolve())
        else:
            dirconfigs = Path(dir_name, "data", "configs")
            if not template.endswith(".txt"):
                template = template + ".txt"
            fp_config = Path(dirconfigs, template)
            if fp_config.exists():
                self._config = str(fp_config.resolve())
            else:
                raise FileNotFoundError(
                    f"Template {template} is not found. Supported templates are:\n"
                    + "\n".join(SMSConfig.get_templates())
                )

    def __repr__(self):
        """Return a string representation of the configuration object."""
        return f'Configuration path: "{self.config}"'

    def __str__(self):
        """Return the configuration path as a string."""
        return self.config

    @property
    def config(self):
        """Return the configuration path."""
        return self._config

    @staticmethod
    def get_templates():
        """
        Return the list of available configuration templates.

        Returns
        -------
        list of str
            List of template names available in the data/configs directory.
        """
        dirconfigs = Path(dir_name, "data", "configs")
        return [str(f.relative_to(dirconfigs)) for f in dirconfigs.glob("**/*.txt")]

config property

Return the configuration path.

__init__(fp=None, template=None)

Initialize a SMSConfig object. If an existing fp is provided, it is used as the configuration file; an error is thrown if the file does not exist. If a template is provided, the configuration file is set to the template file in the data/configs directory. fp and template cannot be both None.

Parameters:

Name Type Description Default
fp Path | str (default: None)

The path to the configuration file.

None
template str (default: None)

The template name of the configuration file.

None
Source code in pysmspp/block.py
def __init__(self, fp: Path | str = None, template: str = None):
    """
    Initialize a SMSConfig object.
    If an existing fp is provided, it is used as the configuration file; an error is thrown if the file does not exist.
    If a template is provided, the configuration file is set to the template file in the data/configs directory.
    fp and template cannot be both None.

    Parameters
    ----------
    fp : Path | str (default: None)
        The path to the configuration file.
    template : str (default: None)
        The template name of the configuration file.
    """
    if fp is None and template is None:
        raise ValueError("Either fp or template must be provided.")
    if fp is not None and template is not None:
        raise ValueError("fp or template cannot be specified together.")

    if template is None:
        fp_p = Path(fp)
        if not fp_p.exists():
            raise FileNotFoundError(f"File {fp} not found.")
        else:
            self._config = str(fp_p.resolve())
    else:
        dirconfigs = Path(dir_name, "data", "configs")
        if not template.endswith(".txt"):
            template = template + ".txt"
        fp_config = Path(dirconfigs, template)
        if fp_config.exists():
            self._config = str(fp_config.resolve())
        else:
            raise FileNotFoundError(
                f"Template {template} is not found. Supported templates are:\n"
                + "\n".join(SMSConfig.get_templates())
            )

__repr__()

Return a string representation of the configuration object.

Source code in pysmspp/block.py
def __repr__(self):
    """Return a string representation of the configuration object."""
    return f'Configuration path: "{self.config}"'

__str__()

Return the configuration path as a string.

Source code in pysmspp/block.py
def __str__(self):
    """Return the configuration path as a string."""
    return self.config

get_templates() staticmethod

Return the list of available configuration templates.

Returns:

Type Description
list of str

List of template names available in the data/configs directory.

Source code in pysmspp/block.py
@staticmethod
def get_templates():
    """
    Return the list of available configuration templates.

    Returns
    -------
    list of str
        List of template names available in the data/configs directory.
    """
    dirconfigs = Path(dir_name, "data", "configs")
    return [str(f.relative_to(dirconfigs)) for f in dirconfigs.glob("**/*.txt")]

SMSFileType

Bases: IntEnum

Enumeration of SMS++ file types.

Defines the different types of files that can be created and managed in SMS++ systems. Each file type serves a specific purpose in the modeling and optimization workflow.

Attributes:

Name Type Description
eProbFile int

Problem file (value 0): Contains both the model block structure and solver configuration. This is the complete specification needed to run an optimization.

eBlockFile int

Block file (value 1): Contains only the model structure with blocks, variables, dimensions, and attributes. No solver configuration included.

eConfigFile int

Configuration file (value 2): Contains only solver settings and parameters. No model structure included.

eSolutionFile int

Solution file (value 3): Contains the optimization results including objective values, variable values, and solver status.

Examples:

>>> network = SMSNetwork(file_type=SMSFileType.eBlockFile)
>>> print(SMSFileType.eProbFile)  # Output: 0
>>> file_type = SMSFileType(1)  # eBlockFile
See Also

SMSNetwork : Uses SMSFileType to specify file format

Source code in pysmspp/block.py
class SMSFileType(IntEnum):
    """
    Enumeration of SMS++ file types.

    Defines the different types of files that can be created and managed in SMS++
    systems. Each file type serves a specific purpose in the modeling and
    optimization workflow.

    Attributes
    ----------
    eProbFile : int
        Problem file (value 0): Contains both the model block structure and
        solver configuration. This is the complete specification needed to run
        an optimization.
    eBlockFile : int
        Block file (value 1): Contains only the model structure with blocks,
        variables, dimensions, and attributes. No solver configuration included.
    eConfigFile : int
        Configuration file (value 2): Contains only solver settings and
        parameters. No model structure included.
    eSolutionFile : int
        Solution file (value 3): Contains the optimization results including
        objective values, variable values, and solver status.

    Examples
    --------
    >>> network = SMSNetwork(file_type=SMSFileType.eBlockFile)
    >>> print(SMSFileType.eProbFile)  # Output: 0
    >>> file_type = SMSFileType(1)  # eBlockFile

    See Also
    --------
    SMSNetwork : Uses SMSFileType to specify file format
    """

    eProbFile = 0  # Problem file: Block and Configuration
    eBlockFile = 1  # Block file
    eConfigFile = 2  # Configuration file
    eSolutionFile = 3  # Solution file

SMSNetwork

Bases: Block

Top-level network container for SMS++ optimization models.

SMSNetwork is the main entry point for creating and managing complete SMS++ models. It extends Block with network-specific functionality including file type management and optimization execution.

An SMSNetwork can contain multiple blocks organized hierarchically to represent complex optimization problems such as unit commitment, investment planning, or multi-stage stochastic problems.

Attributes:

Name Type Description
file_type SMSFileType

The type of SMS++ file (eProbFile, eBlockFile, eConfigFile, or eSolutionFile).

attributes Dict

Inherited from Block. Network-level attributes.

dimensions Dict

Inherited from Block. Network-level dimensions.

variables Dict

Inherited from Block. Network-level variables.

blocks Dict

Inherited from Block. Nested blocks forming the model structure.

Examples:

Create an empty network:

>>> network = SMSNetwork()

Create a network with block file type:

>>> network = SMSNetwork(file_type=SMSFileType.eBlockFile)

Load a network from file:

>>> network = SMSNetwork(fp="model.nc")

Create and optimize a network:

>>> network = SMSNetwork(file_type=SMSFileType.eBlockFile)
>>> network.add("UCBlock", "Block_0", TimeHorizon=24, NumberUnits=1)
>>> result = network.optimize(config, temp_file, output_file)
See Also

Block : Base class for hierarchical components SMSConfig : Configuration manager for SMS++ solvers SMSFileType : Enumeration of file types

Source code in pysmspp/block.py
class SMSNetwork(Block):
    """
    Top-level network container for SMS++ optimization models.

    SMSNetwork is the main entry point for creating and managing complete SMS++
    models. It extends Block with network-specific functionality including file
    type management and optimization execution.

    An SMSNetwork can contain multiple blocks organized hierarchically to represent
    complex optimization problems such as unit commitment, investment planning, or
    multi-stage stochastic problems.

    Attributes
    ----------
    file_type : SMSFileType
        The type of SMS++ file (eProbFile, eBlockFile, eConfigFile, or eSolutionFile).
    attributes : Dict
        Inherited from Block. Network-level attributes.
    dimensions : Dict
        Inherited from Block. Network-level dimensions.
    variables : Dict
        Inherited from Block. Network-level variables.
    blocks : Dict
        Inherited from Block. Nested blocks forming the model structure.

    Examples
    --------
    Create an empty network:

    >>> network = SMSNetwork()

    Create a network with block file type:

    >>> network = SMSNetwork(file_type=SMSFileType.eBlockFile)

    Load a network from file:

    >>> network = SMSNetwork(fp="model.nc")

    Create and optimize a network:

    >>> network = SMSNetwork(file_type=SMSFileType.eBlockFile)
    >>> network.add("UCBlock", "Block_0", TimeHorizon=24, NumberUnits=1)
    >>> result = network.optimize(config, temp_file, output_file)

    See Also
    --------
    Block : Base class for hierarchical components
    SMSConfig : Configuration manager for SMS++ solvers
    SMSFileType : Enumeration of file types
    """

    def __init__(
        self,
        fp: Path | str = "",
        file_type: SMSFileType | int = SMSFileType.eProbFile,
        **kwargs,
    ):
        """
        Initialize an SMSNetwork object.

        Creates a new SMS++ network, either empty, from a file, or with specified
        components. The file_type determines how the network will be stored and used.

        Parameters
        ----------
        fp : Path | str, optional
            Path to a NetCDF file to load the network from. If provided, the network
            is loaded from the file. Default is empty string (create empty network).
        file_type : SMSFileType | int, optional
            The type of SMS++ file to create. Options:
            - eProbFile (0): Problem file with block and configuration
            - eBlockFile (1): Block file only (no configuration)
            - eConfigFile (2): Configuration file only
            - eSolutionFile (3): Solution file
            Default is eProbFile.
        **kwargs : dict
            Additional keyword arguments to pass to the Block constructor for
            dynamic component creation.

        Examples
        --------
        >>> network = SMSNetwork()
        >>> network = SMSNetwork(file_type=SMSFileType.eBlockFile)
        >>> network = SMSNetwork(fp="existing_model.nc")
        """
        if fp:
            super().__init__()
            sms_network = self.from_netcdf(fp)
            self._attributes = sms_network.attributes
            self._dimensions = sms_network.dimensions
            self._variables = sms_network.variables
            self._blocks = sms_network.blocks
        else:
            super().__init__(**kwargs)
            self.file_type = file_type

    def __repr__(self):
        """
        Return a string representation of the SMSNetwork object.

        Returns
        -------
        str
            A formatted string identifying this as an SMSNetwork with its components.
        """
        return f"SMSNetwork Object\n{super().__repr__()}"

    @property
    def file_type(self) -> SMSFileType:
        """Return the file type of the SMS file."""
        return SMSFileType(self.attributes["SMS++_file_type"].value)

    @file_type.setter
    def file_type(self, ft: SMSFileType | int):
        """Set the file type of the SMS file."""
        self.add_attribute("SMS++_file_type", int(ft), force=True)

    @classmethod
    def _from_netcdf(cls, ncfile: nc.Dataset):
        """Deserialize a NetCDF file to create a Block instance with nested sub-blocks."""
        blk = super()._from_netcdf(ncfile)
        file_type = ncfile.getncattr("SMS++_file_type")
        return SMSNetwork(
            file_type=file_type,
            attributes=blk.attributes,
            dimensions=blk.dimensions,
            variables=blk.variables,
            blocks=blk.blocks,
        )

    def print_tree(
        self,
        name: str = None,
        show_dimensions: bool = False,
        show_variables: bool = False,
        show_attributes: bool = False,
        show_all: bool = False,
        _indent: str = "",
        _is_last: bool = True,
        _is_root: bool = True,
    ) -> None:
        """
        Print a tree representation of the SMSNetwork structure.

        This method overrides Block.print_tree() to use "SMSNetwork" as the default name.

        Parameters
        ----------
        name : str, optional
            The name of the network. If not provided, defaults to "SMSNetwork".
        show_dimensions : bool, optional
            Whether to display dimensions (default: False).
        show_variables : bool, optional
            Whether to display variables (default: False).
        show_attributes : bool, optional
            Whether to display attributes (default: False).
        show_all : bool, optional
            If True, show dimensions, variables, and attributes (default: False). Overrides individual show_* flags.
        _indent : str, optional
            Internal parameter for indentation (default: "").
        _is_last : bool, optional
            Internal parameter to track if this is the last child (default: True).
        _is_root : bool, optional
            Internal parameter to track if this is the root node (default: True).

        Examples
        --------
        >>> from pysmspp import SMSNetwork
        >>> net = SMSNetwork(fp="network.nc4")
        >>> net.print_tree()  # Uses "SMSNetwork" as default name
        SMSNetwork
        └── Block_0 [UCBlock]
            ...
        """
        # Use "SMSNetwork" as default name for SMSNetwork objects
        if name is None:
            name = "SMSNetwork"
        if show_all:
            show_dimensions = True
            show_variables = True
            show_attributes = True

        # Call parent class method
        super().print_tree(
            name=name,
            show_dimensions=show_dimensions,
            show_variables=show_variables,
            show_attributes=show_attributes,
            _indent=_indent,
            _is_last=_is_last,
            _is_root=_is_root,
        )

    def optimize(
        self,
        configfile: SMSConfig | Path | str,
        fp_temp: Path | str = "temp.nc",
        fp_log: Path | str = None,
        fp_solution: Path | str = None,
        smspp_solver: SMSPPSolverTool | str = "auto",
        inner_block_name: str = "Block_0",
        logging=True,
        tracking_period=0.1,
        **kwargs,
    ):
        """
        Optimize the SMSNetwork object.

        Parameters
        ----------
        configfile : SMSConfig | Path | str
            The configuration file. If a path is provided, it is first parsed into a SMSConfig object.
        fp_temp : Path | str (default: "temp.nc")
            The path to the temporary file.
        fp_log : Path | str (default: None)
            The path to the log file.
        fp_solution : Path | str (default: None)
            The path to the solution file.
        smspp_tool : SMSPPSolverTool | str (default: "auto")
            The optimization mode. It supports a SMSPPSolverTool or string-based values.
            If string value is passed, the supported values are:

            - "auto": Automatically select the optimization mode by the type of the inner block.
              If UCBlock, then it selects UCBlockSolver.
            - "UCBlockSolver": Use the UCBlockSolver tool.

        inner_block_name : str (default: "Block_0")
            The name of the inner block, to decide on the automatic solver to use.
        logging : bool (default: True)
            Whether to enable logging during optimization.
        tracking_period : float (default: 0.1)
            The period (in seconds) to track optimization progress when logging is enabled.
        kwargs : dict
            Optional arguments to pass to the solver constructor. These can include any additional parameters required by specific solvers.
        """

        # Map block type to default solver (for 'auto' mode)
        default_solver_map = {
            "UCBlock": "UCBlockSolver",
            "InvestmentBlock": "InvestmentBlockTestSolver",
            "SDDPBlock": "InvestmentSolver",
            "TwoStageStochasticBlock": "TSSBSolver",
        }

        # Map solver names to actual solver classes
        solver_factory = {
            "UCBlockSolver": UCBlockSolver,
            "InvestmentBlockTestSolver": InvestmentBlockTestSolver,
            "InvestmentBlockSolver": InvestmentBlockSolver,
            "InvestmentSolver": InvestmentSolver,
            "TSSBSolver": TSSBSolver,
        }

        if isinstance(smspp_solver, str) and smspp_solver == "auto":
            ib = self.blocks[inner_block_name]
            try:
                smspp_solver = default_solver_map[ib.block_type]
            except KeyError:
                raise ValueError(
                    f'"auto" smspp_solver option not supported for block type {ib.block_type}.'
                )
        # Instantiate solver
        if isinstance(smspp_solver, str):
            try:
                solver_class = solver_factory[smspp_solver]
            except KeyError:
                raise ValueError(f"SMS++ tool {smspp_solver} not supported.")

            if not isinstance(configfile, SMSConfig):
                configfile = SMSConfig(configfile)
            smspp_solver = solver_class(
                configfile=str(configfile),
                fp_network=fp_temp,
                fp_solution=fp_solution,
                fp_log=fp_log,
                **kwargs,
            )

        self.to_netcdf(fp_temp, force=True)
        return smspp_solver.optimize(logging=logging, tracking_period=tracking_period)

file_type property writable

Return the file type of the SMS file.

__init__(fp='', file_type=SMSFileType.eProbFile, **kwargs)

Initialize an SMSNetwork object.

Creates a new SMS++ network, either empty, from a file, or with specified components. The file_type determines how the network will be stored and used.

Parameters:

Name Type Description Default
fp Path | str

Path to a NetCDF file to load the network from. If provided, the network is loaded from the file. Default is empty string (create empty network).

''
file_type SMSFileType | int

The type of SMS++ file to create. Options: - eProbFile (0): Problem file with block and configuration - eBlockFile (1): Block file only (no configuration) - eConfigFile (2): Configuration file only - eSolutionFile (3): Solution file Default is eProbFile.

eProbFile
**kwargs dict

Additional keyword arguments to pass to the Block constructor for dynamic component creation.

{}

Examples:

>>> network = SMSNetwork()
>>> network = SMSNetwork(file_type=SMSFileType.eBlockFile)
>>> network = SMSNetwork(fp="existing_model.nc")
Source code in pysmspp/block.py
def __init__(
    self,
    fp: Path | str = "",
    file_type: SMSFileType | int = SMSFileType.eProbFile,
    **kwargs,
):
    """
    Initialize an SMSNetwork object.

    Creates a new SMS++ network, either empty, from a file, or with specified
    components. The file_type determines how the network will be stored and used.

    Parameters
    ----------
    fp : Path | str, optional
        Path to a NetCDF file to load the network from. If provided, the network
        is loaded from the file. Default is empty string (create empty network).
    file_type : SMSFileType | int, optional
        The type of SMS++ file to create. Options:
        - eProbFile (0): Problem file with block and configuration
        - eBlockFile (1): Block file only (no configuration)
        - eConfigFile (2): Configuration file only
        - eSolutionFile (3): Solution file
        Default is eProbFile.
    **kwargs : dict
        Additional keyword arguments to pass to the Block constructor for
        dynamic component creation.

    Examples
    --------
    >>> network = SMSNetwork()
    >>> network = SMSNetwork(file_type=SMSFileType.eBlockFile)
    >>> network = SMSNetwork(fp="existing_model.nc")
    """
    if fp:
        super().__init__()
        sms_network = self.from_netcdf(fp)
        self._attributes = sms_network.attributes
        self._dimensions = sms_network.dimensions
        self._variables = sms_network.variables
        self._blocks = sms_network.blocks
    else:
        super().__init__(**kwargs)
        self.file_type = file_type

__repr__()

Return a string representation of the SMSNetwork object.

Returns:

Type Description
str

A formatted string identifying this as an SMSNetwork with its components.

Source code in pysmspp/block.py
def __repr__(self):
    """
    Return a string representation of the SMSNetwork object.

    Returns
    -------
    str
        A formatted string identifying this as an SMSNetwork with its components.
    """
    return f"SMSNetwork Object\n{super().__repr__()}"

optimize(configfile, fp_temp='temp.nc', fp_log=None, fp_solution=None, smspp_solver='auto', inner_block_name='Block_0', logging=True, tracking_period=0.1, **kwargs)

Optimize the SMSNetwork object.

Parameters:

Name Type Description Default
configfile SMSConfig | Path | str

The configuration file. If a path is provided, it is first parsed into a SMSConfig object.

required
fp_temp Path | str (default: "temp.nc")

The path to the temporary file.

'temp.nc'
fp_log Path | str (default: None)

The path to the log file.

None
fp_solution Path | str (default: None)

The path to the solution file.

None
smspp_tool SMSPPSolverTool | str (default: "auto")

The optimization mode. It supports a SMSPPSolverTool or string-based values. If string value is passed, the supported values are:

  • "auto": Automatically select the optimization mode by the type of the inner block. If UCBlock, then it selects UCBlockSolver.
  • "UCBlockSolver": Use the UCBlockSolver tool.
required
inner_block_name str (default: "Block_0")

The name of the inner block, to decide on the automatic solver to use.

'Block_0'
logging bool (default: True)

Whether to enable logging during optimization.

True
tracking_period float (default: 0.1)

The period (in seconds) to track optimization progress when logging is enabled.

0.1
kwargs dict

Optional arguments to pass to the solver constructor. These can include any additional parameters required by specific solvers.

{}
Source code in pysmspp/block.py
def optimize(
    self,
    configfile: SMSConfig | Path | str,
    fp_temp: Path | str = "temp.nc",
    fp_log: Path | str = None,
    fp_solution: Path | str = None,
    smspp_solver: SMSPPSolverTool | str = "auto",
    inner_block_name: str = "Block_0",
    logging=True,
    tracking_period=0.1,
    **kwargs,
):
    """
    Optimize the SMSNetwork object.

    Parameters
    ----------
    configfile : SMSConfig | Path | str
        The configuration file. If a path is provided, it is first parsed into a SMSConfig object.
    fp_temp : Path | str (default: "temp.nc")
        The path to the temporary file.
    fp_log : Path | str (default: None)
        The path to the log file.
    fp_solution : Path | str (default: None)
        The path to the solution file.
    smspp_tool : SMSPPSolverTool | str (default: "auto")
        The optimization mode. It supports a SMSPPSolverTool or string-based values.
        If string value is passed, the supported values are:

        - "auto": Automatically select the optimization mode by the type of the inner block.
          If UCBlock, then it selects UCBlockSolver.
        - "UCBlockSolver": Use the UCBlockSolver tool.

    inner_block_name : str (default: "Block_0")
        The name of the inner block, to decide on the automatic solver to use.
    logging : bool (default: True)
        Whether to enable logging during optimization.
    tracking_period : float (default: 0.1)
        The period (in seconds) to track optimization progress when logging is enabled.
    kwargs : dict
        Optional arguments to pass to the solver constructor. These can include any additional parameters required by specific solvers.
    """

    # Map block type to default solver (for 'auto' mode)
    default_solver_map = {
        "UCBlock": "UCBlockSolver",
        "InvestmentBlock": "InvestmentBlockTestSolver",
        "SDDPBlock": "InvestmentSolver",
        "TwoStageStochasticBlock": "TSSBSolver",
    }

    # Map solver names to actual solver classes
    solver_factory = {
        "UCBlockSolver": UCBlockSolver,
        "InvestmentBlockTestSolver": InvestmentBlockTestSolver,
        "InvestmentBlockSolver": InvestmentBlockSolver,
        "InvestmentSolver": InvestmentSolver,
        "TSSBSolver": TSSBSolver,
    }

    if isinstance(smspp_solver, str) and smspp_solver == "auto":
        ib = self.blocks[inner_block_name]
        try:
            smspp_solver = default_solver_map[ib.block_type]
        except KeyError:
            raise ValueError(
                f'"auto" smspp_solver option not supported for block type {ib.block_type}.'
            )
    # Instantiate solver
    if isinstance(smspp_solver, str):
        try:
            solver_class = solver_factory[smspp_solver]
        except KeyError:
            raise ValueError(f"SMS++ tool {smspp_solver} not supported.")

        if not isinstance(configfile, SMSConfig):
            configfile = SMSConfig(configfile)
        smspp_solver = solver_class(
            configfile=str(configfile),
            fp_network=fp_temp,
            fp_solution=fp_solution,
            fp_log=fp_log,
            **kwargs,
        )

    self.to_netcdf(fp_temp, force=True)
    return smspp_solver.optimize(logging=logging, tracking_period=tracking_period)

print_tree(name=None, show_dimensions=False, show_variables=False, show_attributes=False, show_all=False, _indent='', _is_last=True, _is_root=True)

Print a tree representation of the SMSNetwork structure.

This method overrides Block.print_tree() to use "SMSNetwork" as the default name.

Parameters:

Name Type Description Default
name str

The name of the network. If not provided, defaults to "SMSNetwork".

None
show_dimensions bool

Whether to display dimensions (default: False).

False
show_variables bool

Whether to display variables (default: False).

False
show_attributes bool

Whether to display attributes (default: False).

False
show_all bool

If True, show dimensions, variables, and attributes (default: False). Overrides individual show_* flags.

False
_indent str

Internal parameter for indentation (default: "").

''
_is_last bool

Internal parameter to track if this is the last child (default: True).

True
_is_root bool

Internal parameter to track if this is the root node (default: True).

True

Examples:

>>> from pysmspp import SMSNetwork
>>> net = SMSNetwork(fp="network.nc4")
>>> net.print_tree()  # Uses "SMSNetwork" as default name
SMSNetwork
└── Block_0 [UCBlock]
    ...
Source code in pysmspp/block.py
def print_tree(
    self,
    name: str = None,
    show_dimensions: bool = False,
    show_variables: bool = False,
    show_attributes: bool = False,
    show_all: bool = False,
    _indent: str = "",
    _is_last: bool = True,
    _is_root: bool = True,
) -> None:
    """
    Print a tree representation of the SMSNetwork structure.

    This method overrides Block.print_tree() to use "SMSNetwork" as the default name.

    Parameters
    ----------
    name : str, optional
        The name of the network. If not provided, defaults to "SMSNetwork".
    show_dimensions : bool, optional
        Whether to display dimensions (default: False).
    show_variables : bool, optional
        Whether to display variables (default: False).
    show_attributes : bool, optional
        Whether to display attributes (default: False).
    show_all : bool, optional
        If True, show dimensions, variables, and attributes (default: False). Overrides individual show_* flags.
    _indent : str, optional
        Internal parameter for indentation (default: "").
    _is_last : bool, optional
        Internal parameter to track if this is the last child (default: True).
    _is_root : bool, optional
        Internal parameter to track if this is the root node (default: True).

    Examples
    --------
    >>> from pysmspp import SMSNetwork
    >>> net = SMSNetwork(fp="network.nc4")
    >>> net.print_tree()  # Uses "SMSNetwork" as default name
    SMSNetwork
    └── Block_0 [UCBlock]
        ...
    """
    # Use "SMSNetwork" as default name for SMSNetwork objects
    if name is None:
        name = "SMSNetwork"
    if show_all:
        show_dimensions = True
        show_variables = True
        show_attributes = True

    # Call parent class method
    super().print_tree(
        name=name,
        show_dimensions=show_dimensions,
        show_variables=show_variables,
        show_attributes=show_attributes,
        _indent=_indent,
        _is_last=_is_last,
        _is_root=_is_root,
    )

Variable

Represents a variable in an SMS++ model.

Variables hold the data arrays and parameters used in SMS++ optimization models. They have a specific type, dimensional structure, and associated data values.

Attributes:

Name Type Description
name str

The name of the variable.

var_type str

The data type of the variable (e.g., "float", "int", "double").

dimensions tuple

The dimensions of the variable as a tuple of dimension names.

data float | list | ndarray

The data values of the variable.

Examples:

>>> var = Variable("MinPower", "float", (), 0.0)
>>> var = Variable("ActivePowerDemand", "float", ("NumberNodes", "TimeHorizon"), np.full((2, 24), 50.0))
Source code in pysmspp/block.py
class Variable:
    """
    Represents a variable in an SMS++ model.

    Variables hold the data arrays and parameters used in SMS++ optimization models.
    They have a specific type, dimensional structure, and associated data values.

    Attributes
    ----------
    name : str
        The name of the variable.
    var_type : str
        The data type of the variable (e.g., "float", "int", "double").
    dimensions : tuple
        The dimensions of the variable as a tuple of dimension names.
    data : float | list | np.ndarray
        The data values of the variable.

    Examples
    --------
    >>> var = Variable("MinPower", "float", (), 0.0)
    >>> var = Variable("ActivePowerDemand", "float", ("NumberNodes", "TimeHorizon"), np.full((2, 24), 50.0))
    """

    name: str
    var_type: str
    dimensions: tuple
    data: float | list | np.ndarray

    def __init__(
        self,
        name: str,
        var_type: str,
        dimensions: tuple,
        data: float | list | np.ndarray,
    ):
        """
        Initialize a Variable object.

        Parameters
        ----------
        name : str
            The name of the variable.
        var_type : str
            The data type of the variable (e.g., "float", "int", "double").
        dimensions : tuple
            The dimensions of the variable. Use empty tuple () for scalar values.
        data : float | list | np.ndarray
            The data values of the variable.
        """
        if dimensions is None:
            dimensions = ()
        self.name = name
        self.var_type = var_type
        self.dimensions = dimensions
        self.data = data

    def __repr__(self) -> str:
        """Return detailed representation of the variable."""
        data_repr = (
            f"{self.data!r}" if not isinstance(self.data, np.ndarray) else "array(...)"
        )
        return f"Variable(name={self.name!r}, var_type={self.var_type!r}, dimensions={self.dimensions!r}, data={data_repr})"

    def plot(self, ax=None, kind: str = "auto", **kwargs):
        """
        Plot the variable data.

        The plot type depends on the variable dimensionality and *kind*:

        - **Scalar** (0-D): bar chart.
        - **1-D array**: line plot with the dimension name on the x-axis.
        - **2-D array**: heatmap (``imshow``) with a colorbar when
          ``kind="heatmap"``, or a line plot where each row is a separate
          line with a legend when ``kind="line"``.
          ``kind="auto"`` defaults to ``"heatmap"``.

        Parameters
        ----------
        ax : matplotlib.axes.Axes, optional
            Axes to draw on. If *None* a new figure and axes are created.
        kind : str, optional
            Plot style for 2-D variables. Accepted values are ``"auto"``,
            ``"heatmap"`` (default), and ``"line"``. Ignored for 0-D and 1-D
            variables.
        **kwargs : dict
            Additional keyword arguments forwarded to the underlying
            matplotlib function (``bar``, ``plot``, or ``imshow``).

        Returns
        -------
        matplotlib.axes.Axes
            The axes containing the plot.

        Raises
        ------
        ValueError
            If the variable has more than 2 dimensions, or an unsupported
            *kind* is given.

        Examples
        --------
        >>> var = Variable("ActivePowerDemand", "float", ("NumberNodes", "TimeHorizon"), data)
        >>> ax = var.plot()
        >>> ax = var.plot(kind="line")
        """
        import matplotlib.pyplot as plt

        data = np.asarray(self.data)

        if ax is None:
            _, ax = plt.subplots()

        if data.ndim == 0:
            ax.bar([self.name], [float(data)], **kwargs)
            ax.set_title(self.name)
        elif data.ndim == 1:
            ax.plot(data, **kwargs)
            ax.set_xlabel(self.dimensions[0])
            ax.set_ylabel(self.name)
            ax.set_title(self.name)
        elif data.ndim == 2:
            resolved_kind = "heatmap" if kind == "auto" else kind
            if resolved_kind == "heatmap":
                im = ax.imshow(data, aspect="auto", **kwargs)
                ax.set_xlabel(self.dimensions[1])
                ax.set_ylabel(self.dimensions[0])
                ax.set_title(self.name)
                plt.colorbar(im, ax=ax)
            elif resolved_kind == "line":
                for i, row in enumerate(data):
                    ax.plot(row, label=f"{self.dimensions[0]}={i}", **kwargs)
                ax.set_xlabel(self.dimensions[1])
                ax.set_ylabel(self.name)
                ax.set_title(self.name)
                ax.legend()
            else:
                raise ValueError(
                    f"Unsupported kind '{kind}'. Use 'auto', 'heatmap', or 'line'."
                )
        else:
            raise ValueError(
                f"Cannot plot variable '{self.name}' with {data.ndim} dimensions. "
                "Only 0-, 1-, and 2-D variables are supported."
            )

        return ax

__init__(name, var_type, dimensions, data)

Initialize a Variable object.

Parameters:

Name Type Description Default
name str

The name of the variable.

required
var_type str

The data type of the variable (e.g., "float", "int", "double").

required
dimensions tuple

The dimensions of the variable. Use empty tuple () for scalar values.

required
data float | list | ndarray

The data values of the variable.

required
Source code in pysmspp/block.py
def __init__(
    self,
    name: str,
    var_type: str,
    dimensions: tuple,
    data: float | list | np.ndarray,
):
    """
    Initialize a Variable object.

    Parameters
    ----------
    name : str
        The name of the variable.
    var_type : str
        The data type of the variable (e.g., "float", "int", "double").
    dimensions : tuple
        The dimensions of the variable. Use empty tuple () for scalar values.
    data : float | list | np.ndarray
        The data values of the variable.
    """
    if dimensions is None:
        dimensions = ()
    self.name = name
    self.var_type = var_type
    self.dimensions = dimensions
    self.data = data

__repr__()

Return detailed representation of the variable.

Source code in pysmspp/block.py
def __repr__(self) -> str:
    """Return detailed representation of the variable."""
    data_repr = (
        f"{self.data!r}" if not isinstance(self.data, np.ndarray) else "array(...)"
    )
    return f"Variable(name={self.name!r}, var_type={self.var_type!r}, dimensions={self.dimensions!r}, data={data_repr})"

plot(ax=None, kind='auto', **kwargs)

Plot the variable data.

The plot type depends on the variable dimensionality and kind:

  • Scalar (0-D): bar chart.
  • 1-D array: line plot with the dimension name on the x-axis.
  • 2-D array: heatmap (imshow) with a colorbar when kind="heatmap", or a line plot where each row is a separate line with a legend when kind="line". kind="auto" defaults to "heatmap".

Parameters:

Name Type Description Default
ax Axes

Axes to draw on. If None a new figure and axes are created.

None
kind str

Plot style for 2-D variables. Accepted values are "auto", "heatmap" (default), and "line". Ignored for 0-D and 1-D variables.

'auto'
**kwargs dict

Additional keyword arguments forwarded to the underlying matplotlib function (bar, plot, or imshow).

{}

Returns:

Type Description
Axes

The axes containing the plot.

Raises:

Type Description
ValueError

If the variable has more than 2 dimensions, or an unsupported kind is given.

Examples:

>>> var = Variable("ActivePowerDemand", "float", ("NumberNodes", "TimeHorizon"), data)
>>> ax = var.plot()
>>> ax = var.plot(kind="line")
Source code in pysmspp/block.py
def plot(self, ax=None, kind: str = "auto", **kwargs):
    """
    Plot the variable data.

    The plot type depends on the variable dimensionality and *kind*:

    - **Scalar** (0-D): bar chart.
    - **1-D array**: line plot with the dimension name on the x-axis.
    - **2-D array**: heatmap (``imshow``) with a colorbar when
      ``kind="heatmap"``, or a line plot where each row is a separate
      line with a legend when ``kind="line"``.
      ``kind="auto"`` defaults to ``"heatmap"``.

    Parameters
    ----------
    ax : matplotlib.axes.Axes, optional
        Axes to draw on. If *None* a new figure and axes are created.
    kind : str, optional
        Plot style for 2-D variables. Accepted values are ``"auto"``,
        ``"heatmap"`` (default), and ``"line"``. Ignored for 0-D and 1-D
        variables.
    **kwargs : dict
        Additional keyword arguments forwarded to the underlying
        matplotlib function (``bar``, ``plot``, or ``imshow``).

    Returns
    -------
    matplotlib.axes.Axes
        The axes containing the plot.

    Raises
    ------
    ValueError
        If the variable has more than 2 dimensions, or an unsupported
        *kind* is given.

    Examples
    --------
    >>> var = Variable("ActivePowerDemand", "float", ("NumberNodes", "TimeHorizon"), data)
    >>> ax = var.plot()
    >>> ax = var.plot(kind="line")
    """
    import matplotlib.pyplot as plt

    data = np.asarray(self.data)

    if ax is None:
        _, ax = plt.subplots()

    if data.ndim == 0:
        ax.bar([self.name], [float(data)], **kwargs)
        ax.set_title(self.name)
    elif data.ndim == 1:
        ax.plot(data, **kwargs)
        ax.set_xlabel(self.dimensions[0])
        ax.set_ylabel(self.name)
        ax.set_title(self.name)
    elif data.ndim == 2:
        resolved_kind = "heatmap" if kind == "auto" else kind
        if resolved_kind == "heatmap":
            im = ax.imshow(data, aspect="auto", **kwargs)
            ax.set_xlabel(self.dimensions[1])
            ax.set_ylabel(self.dimensions[0])
            ax.set_title(self.name)
            plt.colorbar(im, ax=ax)
        elif resolved_kind == "line":
            for i, row in enumerate(data):
                ax.plot(row, label=f"{self.dimensions[0]}={i}", **kwargs)
            ax.set_xlabel(self.dimensions[1])
            ax.set_ylabel(self.name)
            ax.set_title(self.name)
            ax.legend()
        else:
            raise ValueError(
                f"Unsupported kind '{kind}'. Use 'auto', 'heatmap', or 'line'."
            )
    else:
        raise ValueError(
            f"Cannot plot variable '{self.name}' with {data.ndim} dimensions. "
            "Only 0-, 1-, and 2-D variables are supported."
        )

    return ax

get_attr_field(block_type, attr_name, attr_value=None, col_name=None)

Return the entry or the entire attribute row (pandas.Series) from block configuration.

Parameters:

Name Type Description Default
block_type str

The type of the block.

required
attr_name str

The name of the attribute.

required
attr_value any

The value used to infer the smspp_object type when block_type is "Block". If attr_value is an instance of Block, Variable, Dimension or Attribute, the corresponding type name is returned. If attr_value is None or any other type when block_type is "Block", it is treated as an attribute, a warning is issued, and "Attribute" is returned. For other block_type values, this parameter is ignored for type inference.

None
col_name str

The specific entry to retrieve. If None, returns the entire row.

None

Returns:

Type Description
str or Series

The requested entry or entire attribute row (pandas.Series).

Source code in pysmspp/block.py
def get_attr_field(
    block_type: str, attr_name: str, attr_value=None, col_name: str = None
):
    """
    Return the entry or the entire attribute row (pandas.Series) from block configuration.

    Parameters
    ----------
    block_type : str
        The type of the block.
    attr_name : str
        The name of the attribute.
    attr_value : any, optional
        The value used to infer the smspp_object type when ``block_type`` is
        ``"Block"``. If ``attr_value`` is an instance of ``Block``, ``Variable``,
        ``Dimension`` or ``Attribute``, the corresponding type name is returned.
        If ``attr_value`` is ``None`` or any other type when ``block_type`` is
        ``"Block"``, it is treated as an attribute, a warning is issued, and
        ``"Attribute"`` is returned. For other ``block_type`` values, this
        parameter is ignored for type inference.
    col_name : str, optional
        The specific entry to retrieve. If None, returns the entire row.

    Returns
    -------
    str or pandas.Series
        The requested entry or entire attribute row (pandas.Series).
    """
    if (block_type == "Block") or (block_type is None) or (block_type not in blocks):
        if (
            (block_type is not None)
            and (block_type not in blocks)
            and (block_type != "Block")
        ):
            warnings.warn(
                f"Block type {block_type} not found in blocks configuration. "
                + "Type inference will be based on attribute value only."
            )
        if isinstance(attr_value, Block):
            return "Block"
        elif isinstance(attr_value, Variable):
            return "Variable"
        elif isinstance(attr_value, Dimension):
            return "Dimension"
        elif isinstance(attr_value, Attribute):
            return "Attribute"
        elif attr_value is not None:
            warnings.warn(
                f"Non-specified {attr_name} with value {attr_value} treated as attribute."
            )
            return "Attribute"
        else:
            raise ValueError(
                f"Cannot infer type of {attr_name} with value {attr_value} in Block."
            )

    block_attrs = blocks[block_type].query("smspp_object == 'Block'")
    simple_attrs = blocks[block_type].query("smspp_object != 'Block'")

    if attr_name in simple_attrs.index:
        attr = attr_name
    else:
        attr_sel = block_attrs.loc[
            block_attrs.index.to_series().map(lambda x: attr_name.startswith(x))
        ]
        if attr_sel.shape[0] == 1:
            attr = attr_sel.index[0]
        elif attr_sel.empty:
            raise ValueError(f"Attribute {attr_name} not found in block {block_type}.")
        else:
            raise ValueError(
                f"Ambiguous attribute {attr_name} in block {block_type}."
                + f"Possible types: {attr_sel.index.tolist()}."
            )

    if col_name is None:
        return blocks[block_type].loc[attr]
    else:
        return blocks[block_type].at[attr, col_name]

smspp_tools

InvestmentBlockSolver

Bases: SMSPPSolverTool

Class to interact with the InvestmentBlockSolver tool from SMS++, with executable file "investmentblock_solver".

Source code in pysmspp/smspp_tools.py
class InvestmentBlockSolver(SMSPPSolverTool):
    """
    Class to interact with the InvestmentBlockSolver tool from SMS++, with executable file "investmentblock_solver".
    """

    def __init__(
        self,
        solver_path: Path | str = "investmentblock_solver",
        fp_network: Path | str = None,
        configfile: Path | str = None,
        fp_log: Path | str = None,
        fp_solution: Path | str = None,
        configsolution: Path | str = None,
        help_option: str = "-h",
        **kwargs,
    ):
        """
        The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.
        """
        super().__init__(
            solver_path=solver_path,
            fp_network=fp_network,
            configfile=configfile,
            fp_log=fp_log,
            fp_solution=fp_solution,
            configsolution=configsolution,
            help_option=help_option,
            **kwargs,
        )
        if "v" not in self._kwargs:
            self._kwargs["v"] = "1"

    def parse_solver_log(
        self,
    ):  # TODO: needs revision to better capture the output
        """
        Check the output of the InvestmentBlockSolver.
        It will extract the status, upper bound, lower bound, and objective value from the log.

        Parameters
        ----------
        log : str
            The path to the solution file.
        """
        if self._log is None:
            raise ValueError("Optimization was not launched.")

        res = re.search(r"Fi\* = (.*)\n", self._log)

        if not res:  # if success not found
            self._status = "Failed"
            self._objective_value = np.nan
            self._lower_bound = np.nan
            self._upper_bound = np.nan
            return

        self._objective_value = float(res.group(1).replace("\r", ""))

        res = re.search("Solver status: (.*)\n", self._log)
        smspp_status = res.group(1).replace("\r", "")

        if np.isfinite(self._objective_value):
            self._status = f"Success ({smspp_status})"
        else:
            self._status = f"Failed ({smspp_status})"

        self._lower_bound = np.nan
        self._upper_bound = np.nan

__init__(solver_path='investmentblock_solver', fp_network=None, configfile=None, fp_log=None, fp_solution=None, configsolution=None, help_option='-h', **kwargs)

The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.

Source code in pysmspp/smspp_tools.py
def __init__(
    self,
    solver_path: Path | str = "investmentblock_solver",
    fp_network: Path | str = None,
    configfile: Path | str = None,
    fp_log: Path | str = None,
    fp_solution: Path | str = None,
    configsolution: Path | str = None,
    help_option: str = "-h",
    **kwargs,
):
    """
    The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.
    """
    super().__init__(
        solver_path=solver_path,
        fp_network=fp_network,
        configfile=configfile,
        fp_log=fp_log,
        fp_solution=fp_solution,
        configsolution=configsolution,
        help_option=help_option,
        **kwargs,
    )
    if "v" not in self._kwargs:
        self._kwargs["v"] = "1"

parse_solver_log()

Check the output of the InvestmentBlockSolver. It will extract the status, upper bound, lower bound, and objective value from the log.

Parameters:

Name Type Description Default
log str

The path to the solution file.

required
Source code in pysmspp/smspp_tools.py
def parse_solver_log(
    self,
):  # TODO: needs revision to better capture the output
    """
    Check the output of the InvestmentBlockSolver.
    It will extract the status, upper bound, lower bound, and objective value from the log.

    Parameters
    ----------
    log : str
        The path to the solution file.
    """
    if self._log is None:
        raise ValueError("Optimization was not launched.")

    res = re.search(r"Fi\* = (.*)\n", self._log)

    if not res:  # if success not found
        self._status = "Failed"
        self._objective_value = np.nan
        self._lower_bound = np.nan
        self._upper_bound = np.nan
        return

    self._objective_value = float(res.group(1).replace("\r", ""))

    res = re.search("Solver status: (.*)\n", self._log)
    smspp_status = res.group(1).replace("\r", "")

    if np.isfinite(self._objective_value):
        self._status = f"Success ({smspp_status})"
    else:
        self._status = f"Failed ({smspp_status})"

    self._lower_bound = np.nan
    self._upper_bound = np.nan

InvestmentBlockTestSolver

Bases: SMSPPSolverTool

Class to interact with the InvestmentBlockTestSolver tool from SMS++, with executable file "InvestmentBlock_test".

Source code in pysmspp/smspp_tools.py
class InvestmentBlockTestSolver(SMSPPSolverTool):
    """
    Class to interact with the InvestmentBlockTestSolver tool from SMS++, with executable file "InvestmentBlock_test".
    """

    def __init__(
        self,
        solver_path: Path | str = "InvestmentBlock_test",
        fp_network: Path | str = None,
        configfile: Path | str = None,
        fp_log: Path | str = None,
        fp_solution: Path | str = None,
        configsolution: Path | str = None,
        help_option: str = "-h",
        **kwargs,
    ):
        """
        The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.
        """
        super().__init__(
            solver_path=solver_path,
            fp_network=fp_network,
            configfile=configfile,
            fp_log=fp_log,
            fp_solution=fp_solution,
            configsolution=configsolution,
            help_option=help_option,
            **kwargs,
        )
        if "v" not in self._kwargs:
            self._kwargs["v"] = "1"

    def parse_solver_log(
        self,
    ):  # TODO: needs revision to better capture the output
        """
        Check the output of the InvestmentBlockTestSolver.
        It will extract the status, upper bound, lower bound, and objective value from the log.

        Parameters
        ----------
        log : str
            The path to the solution file.
        """
        if self._log is None:
            raise ValueError("Optimization was not launched.")

        res = re.search(r"Fi\* = (.*)\n", self._log)

        if not res:  # if success not found
            self._status = "Failed"
            self._objective_value = np.nan
            self._lower_bound = np.nan
            self._upper_bound = np.nan
            return

        self._objective_value = float(res.group(1).replace("\r", ""))

        res = re.search("Solver status: (.*)\n", self._log)
        smspp_status = res.group(1).replace("\r", "")

        if np.isfinite(self._objective_value):
            self._status = f"Success ({smspp_status})"
        else:
            self._status = f"Failed ({smspp_status})"

        self._lower_bound = np.nan
        self._upper_bound = np.nan

__init__(solver_path='InvestmentBlock_test', fp_network=None, configfile=None, fp_log=None, fp_solution=None, configsolution=None, help_option='-h', **kwargs)

The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.

Source code in pysmspp/smspp_tools.py
def __init__(
    self,
    solver_path: Path | str = "InvestmentBlock_test",
    fp_network: Path | str = None,
    configfile: Path | str = None,
    fp_log: Path | str = None,
    fp_solution: Path | str = None,
    configsolution: Path | str = None,
    help_option: str = "-h",
    **kwargs,
):
    """
    The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.
    """
    super().__init__(
        solver_path=solver_path,
        fp_network=fp_network,
        configfile=configfile,
        fp_log=fp_log,
        fp_solution=fp_solution,
        configsolution=configsolution,
        help_option=help_option,
        **kwargs,
    )
    if "v" not in self._kwargs:
        self._kwargs["v"] = "1"

parse_solver_log()

Check the output of the InvestmentBlockTestSolver. It will extract the status, upper bound, lower bound, and objective value from the log.

Parameters:

Name Type Description Default
log str

The path to the solution file.

required
Source code in pysmspp/smspp_tools.py
def parse_solver_log(
    self,
):  # TODO: needs revision to better capture the output
    """
    Check the output of the InvestmentBlockTestSolver.
    It will extract the status, upper bound, lower bound, and objective value from the log.

    Parameters
    ----------
    log : str
        The path to the solution file.
    """
    if self._log is None:
        raise ValueError("Optimization was not launched.")

    res = re.search(r"Fi\* = (.*)\n", self._log)

    if not res:  # if success not found
        self._status = "Failed"
        self._objective_value = np.nan
        self._lower_bound = np.nan
        self._upper_bound = np.nan
        return

    self._objective_value = float(res.group(1).replace("\r", ""))

    res = re.search("Solver status: (.*)\n", self._log)
    smspp_status = res.group(1).replace("\r", "")

    if np.isfinite(self._objective_value):
        self._status = f"Success ({smspp_status})"
    else:
        self._status = f"Failed ({smspp_status})"

    self._lower_bound = np.nan
    self._upper_bound = np.nan

InvestmentSolver

Bases: SMSPPSolverTool

Class to interact with the InvestmentSolver tool from SMS++, with name "investment_solver".

Source code in pysmspp/smspp_tools.py
class InvestmentSolver(SMSPPSolverTool):
    """
    Class to interact with the InvestmentSolver tool from SMS++, with name "investment_solver".
    """

    def __init__(
        self,
        solver_path: Path | str = "investment_solver",
        fp_network: Path | str = None,
        configfile: Path | str = None,
        fp_log: Path | str = None,
        fp_solution: Path | str = None,
        configsolution: Path | str = None,
        help_option: str = "-h",
        **kwargs,
    ):
        """
        The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.
        """
        super().__init__(
            solver_path=solver_path,
            fp_network=fp_network,
            configfile=configfile,
            fp_log=fp_log,
            fp_solution=fp_solution,
            configsolution=configsolution,
            help_option=help_option,
            **kwargs,
        )

__init__(solver_path='investment_solver', fp_network=None, configfile=None, fp_log=None, fp_solution=None, configsolution=None, help_option='-h', **kwargs)

The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.

Source code in pysmspp/smspp_tools.py
def __init__(
    self,
    solver_path: Path | str = "investment_solver",
    fp_network: Path | str = None,
    configfile: Path | str = None,
    fp_log: Path | str = None,
    fp_solution: Path | str = None,
    configsolution: Path | str = None,
    help_option: str = "-h",
    **kwargs,
):
    """
    The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.
    """
    super().__init__(
        solver_path=solver_path,
        fp_network=fp_network,
        configfile=configfile,
        fp_log=fp_log,
        fp_solution=fp_solution,
        configsolution=configsolution,
        help_option=help_option,
        **kwargs,
    )

SDDPSolver

Bases: SMSPPSolverTool

Class to interact with the SDDPSolver tool from SMS++, with name "sddp_solver".

Source code in pysmspp/smspp_tools.py
class SDDPSolver(SMSPPSolverTool):
    """
    Class to interact with the SDDPSolver tool from SMS++, with name "sddp_solver".
    """

    def __init__(
        self,
        solver_path: Path | str = "sddp_solver",
        fp_network: Path | str = None,
        configfile: Path | str = None,
        fp_log: Path | str = None,
        fp_solution: Path | str = None,
        configsolution: Path | str = None,
        help_option: str = "-h",
        **kwargs,
    ):
        """
        The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.
        """
        super().__init__(
            solver_path=solver_path,
            fp_network=fp_network,
            configfile=configfile,
            fp_log=fp_log,
            fp_solution=fp_solution,
            configsolution=configsolution,
            help_option=help_option,
            **kwargs,
        )

    def parse_solver_log(
        self,
    ):  # TODO: needs revision to better capture the output
        """
        Check the output of the SDDPSolver.
        It will extract the status, upper bound, lower bound, and objective value from the log.

        Parameters
        ----------
        log : str
            The path to the solution file.
        """
        if self._log is None:
            raise ValueError("Optimization was not launched.")

        res = re.search("Solution value: (.*)\n", self._log)

        if not res:  # if success not found
            self._status = "Failed"
            self._objective_value = np.nan
            self._lower_bound = np.nan
            self._upper_bound = np.nan
            return

        self._objective_value = float(res.group(1).replace("\r", ""))

        res = re.search("Solver status: (.*)\n", self._log)
        smspp_status = res.group(1).replace("\r", "")

        if np.isfinite(self._objective_value):
            self._status = f"Success ({smspp_status})"
        else:
            self._status = f"Failed ({smspp_status})"

        self._lower_bound = np.nan
        self._upper_bound = np.nan

__init__(solver_path='sddp_solver', fp_network=None, configfile=None, fp_log=None, fp_solution=None, configsolution=None, help_option='-h', **kwargs)

The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.

Source code in pysmspp/smspp_tools.py
def __init__(
    self,
    solver_path: Path | str = "sddp_solver",
    fp_network: Path | str = None,
    configfile: Path | str = None,
    fp_log: Path | str = None,
    fp_solution: Path | str = None,
    configsolution: Path | str = None,
    help_option: str = "-h",
    **kwargs,
):
    """
    The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.
    """
    super().__init__(
        solver_path=solver_path,
        fp_network=fp_network,
        configfile=configfile,
        fp_log=fp_log,
        fp_solution=fp_solution,
        configsolution=configsolution,
        help_option=help_option,
        **kwargs,
    )

parse_solver_log()

Check the output of the SDDPSolver. It will extract the status, upper bound, lower bound, and objective value from the log.

Parameters:

Name Type Description Default
log str

The path to the solution file.

required
Source code in pysmspp/smspp_tools.py
def parse_solver_log(
    self,
):  # TODO: needs revision to better capture the output
    """
    Check the output of the SDDPSolver.
    It will extract the status, upper bound, lower bound, and objective value from the log.

    Parameters
    ----------
    log : str
        The path to the solution file.
    """
    if self._log is None:
        raise ValueError("Optimization was not launched.")

    res = re.search("Solution value: (.*)\n", self._log)

    if not res:  # if success not found
        self._status = "Failed"
        self._objective_value = np.nan
        self._lower_bound = np.nan
        self._upper_bound = np.nan
        return

    self._objective_value = float(res.group(1).replace("\r", ""))

    res = re.search("Solver status: (.*)\n", self._log)
    smspp_status = res.group(1).replace("\r", "")

    if np.isfinite(self._objective_value):
        self._status = f"Success ({smspp_status})"
    else:
        self._status = f"Failed ({smspp_status})"

    self._lower_bound = np.nan
    self._upper_bound = np.nan

SMSPPSolverTool

Base class for the SMS++ solver tools.

Source code in pysmspp/smspp_tools.py
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
class SMSPPSolverTool:
    """
    Base class for the SMS++ solver tools.
    """

    def __init__(
        self,
        solver_path: Path | str,
        fp_network: Path | str = None,
        configfile: Path | str = None,
        fp_log: Path | str = None,
        fp_solution: Path | str = None,
        configsolution: Path | str = None,
        help_option: str = "-h",
        shell: bool = False,
        **kwargs,
    ):
        """
        Constructor for an abstract SMSPPSolverTool. Option arguments coincide with the options of the SMS++ solver tools. Additional options can be passed through kwargs. Option "-o" is automatically added to kwargs with value None if not provided, to allow logging the solution.

        Parameters
        ----------
        solver_path : str
            The name or path of the executable file.
        fp_network : Path | str, optional
            Path to the SMSpp network to solve, by default None.
            When provided, automatically the option "-p" is added to the executable call to specify the folder of the network file.
        configfile : Path | str, optional
            Path to the configuration file, by default None.
            This option specifies the solver configuration with option "-S" when provided.
            The folder of the configuration file is also specified automatically with option "-c" when provided.
        fp_log : Path | str, optional
            When provided, the solver log is saved to the specified log file, by default None.
        fp_solution : Path | str, optional
            Path to the solution file, by default None.
            When provided, option "-O" is added to the executable call to specify the output solution file.
        configsolution : Path | str, optional
            Path to the configuration solution file, by default None.
            When provided, option "-C" is added to the executable call to specify the configuration solution file.
        help_option : str, optional
            The option to display the help message, by default "-h".
        shell : bool, optional
            Whether to execute the command through the shell. Defaults to False.
        **kwargs
            Additional keyword arguments to pass as options to the function.
            The keys of the kwargs should be the option name, and the value should be the option value.
            For example, if the function has an option "-x" that takes a value, the kwargs should include {"x": value}.
        """
        if isinstance(solver_path, Path):
            self._solver_path = str(solver_path.resolve())
        else:
            self._solver_path = str(solver_path)
        self._help_option = help_option

        self.fp_network = (
            None if fp_network is None else str(Path(fp_network).resolve())
        )
        self.configfile = (
            None if configfile is None else str(Path(configfile).resolve())
        )
        self.configsolution = (
            None if configsolution is None else str(Path(configsolution).resolve())
        )
        self.fp_log = None if fp_log is None else str(Path(fp_log).resolve())
        self.fp_solution = (
            None if fp_solution is None else str(Path(fp_solution).resolve())
        )

        self._shell = shell

        self._status = None
        self._log = None
        self._objective_value = None
        self._lower_bound = None
        self._upper_bound = None
        self._solution = None
        self._subprocess_time = None
        self._solution_time = None
        self._computational_time = None
        self._kwargs = kwargs

        if "c" in self._kwargs:
            raise ValueError(
                "Option 'c' is reserved for the configuration file directory."
            )
        if "p" in self._kwargs:
            raise ValueError("Option 'p' is reserved for the network file directory.")

    def calculate_executable_call(self):
        """
        Generate the standard command-line call for SMS++ solvers and can be customized by subclasses.

        Returns
        -------
        list[str]
            The command array to execute the solver.
        """
        if self.configfile is None:
            raise ValueError("configfile must be provided (non-None).")
        if self.fp_network is None:
            raise ValueError("fp_network must be provided (non-None).")
        configdir, configfile = os.path.split(self.configfile)
        networkdir, networkfile = os.path.split(self.fp_network)
        command = [
            self._solver_path,
            networkfile,
            "-S",
            configfile,
        ]
        if len(configdir) > 0:
            command += ["-c", os.path.join(configdir, "")]
        if len(networkdir) > 0:
            command += ["-p", os.path.join(networkdir, "")]
        if self.fp_solution is not None:
            command += ["-O", self.fp_solution]
        if self.configsolution is not None:
            command += ["-C", self.configsolution]

        for option, value in self._kwargs.items():
            if value == "" or value is None:
                command += [f"-{option}"]
            else:
                command += [f"-{option}", str(value)]

        return command

    def __repr__(self):
        """
        Return a string representation of the solver tool.

        Returns
        -------
        str
            A formatted string showing key solver properties.
        """
        return f"{type(self).__name__}\n\t\n\tsolver_name={self._solver_path}\n\tstatus={self.status}\n\tconfigfile={self.configfile}\n\tfp_network={self.fp_network}\n\tfp_solution={self.fp_solution}"

    def help(self, print_message=True):
        """
        Print the help message of the SMS++ solver tool.

        >>> solver.help()

        Parameters
        ----------
        print_message : bool, optional
            Whether to print the message, by default True.

        Returns
        -------
        The help message.
        """
        result = subprocess.run(
            [self._solver_path, self._help_option],
            capture_output=True,
            shell=self._shell,
        )
        msg = result.stdout.decode("utf-8") + os.linesep + result.stderr.decode("utf-8")
        if print_message:
            print(msg)
        return msg

    def optimize(self, logging=True, tracking_period=0.1):
        """
        Run the SMSPP Solver tool.

        Parameters
        ----------
        logging : bool
            When true, logging is provided, including the executable call.
        tracking_period : float
            Delay in seconds between resource usage tracking samples.
        """
        from pysmspp import SMSNetwork

        if not Path(self.configfile).exists():
            raise FileNotFoundError(
                f"Configuration file {self.configfile} does not exist."
            )
        if not Path(self.fp_network).exists():
            raise FileNotFoundError(f"Network file {self.fp_network} does not exist.")

        command_raw = self.calculate_executable_call()
        command_str = " ".join(command_raw)
        command = command_raw if not self._shell else command_str

        start_time = time.time()
        if logging:
            print(f"Executing command:\n{command_str}\n")

        process = psutil.Popen(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            shell=self._shell,
        )
        pipe_messages = queue.Queue()
        stdout_thread = threading.Thread(
            target=_enqueue_pipe_lines,
            args=(process.stdout, "stdout", pipe_messages),
            daemon=True,
        )
        stderr_thread = threading.Thread(
            target=_enqueue_pipe_lines,
            args=(process.stderr, "stderr", pipe_messages),
            daemon=True,
        )
        stdout_thread.start()
        stderr_thread.start()
        process.cpu_percent()  # initialize cpu percent calculation

        self._log = ""
        log_error = ""

        peak_memory = 0
        peak_cpu = 0

        loop = True
        while loop:
            if process.poll() is not None:
                loop = False
            else:
                try:
                    # capture resource usage when process is active
                    mem = process.memory_info().rss
                    cpu = process.cpu_percent()

                    # track the peak utilization of the process
                    if mem > peak_memory:
                        peak_memory = mem
                    if cpu > peak_cpu:
                        peak_cpu = cpu
                except psutil.NoSuchProcess:
                    pass

            # read from process without stopping it
            msg_out, msg_err = _drain_pipe_messages(pipe_messages, logging)

            self._log += msg_out + msg_err
            log_error += msg_err

            time.sleep(tracking_period)

        # finalize logging
        stdout_thread.join()
        stderr_thread.join()
        msg_out, msg_err = _drain_pipe_messages(pipe_messages, logging)

        self._log += msg_out + msg_err
        log_error += msg_err

        # add memory info to logging
        self._subprocess_time = time.time() - start_time

        msg = f"Peak CPU Usage: {peak_cpu:.2f} %"
        msg += f"\nPeak Memory Usage: {peak_memory / (1024**2):.2f} MB"
        msg += f"\nTotal Time: {self._subprocess_time:.2f} seconds\n"

        self._log += msg
        if logging:
            print(msg)

        self.parse_solver_log()

        if process.returncode != 0:
            raise ValueError(
                f"Failed to run {self._solver_path} with error log:\n{log_error}\n\nFull log:\n{self._log}"
            )

        # write output to file, if option passed
        if self.fp_log is not None:
            Path(self.fp_log).parent.mkdir(parents=True, exist_ok=True)
            with open(self.fp_log, "w") as f:
                f.write(self._log)

        # sets the solution object
        start_solution_time = time.time()
        if self.fp_solution is not None:
            if Path(self.fp_solution).exists():
                self._solution = SMSNetwork(self.fp_solution)
            else:
                raise FileNotFoundError(
                    f"solution file {self.fp_solution} does not exist."
                )
        else:
            self._solution = None
        self._solution_time = time.time() - start_solution_time
        self._computational_time = time.time() - start_time

        return self

    def is_available(self):
        """
        Check if the SMS++ tool is available in the PATH.

        Parameters
        ----------
        shell : bool, optional
            Whether to execute the command through the shell. Defaults to False.

        Returns
        -------
        bool
            True if the tool is available, False otherwise.
        """
        try:
            proc = subprocess.run(
                [self._solver_path, self._help_option],
                check=False,
                shell=self._shell,
            )
            return proc.returncode == 0
        except FileNotFoundError:
            return False
        except Exception as e:
            logger.warning(f"Error checking availability of {self._solver_path}: {e}")
            return False

    def parse_solver_log(self):
        """
        Check the output of the Solver.
        It will extract the status, upper bound, lower bound, and objective value from the log.

        Parameters
        ----------
        log : str
            The path to the solution file.
        """
        if self._log is None:
            raise ValueError("Optimization was not launched.")

        res = re.search("Status = (.*)\n", self._log)

        if not res:  # if success not found
            self._status = "Failed"
            self._objective_value = np.nan
            self._lower_bound = np.nan
            self._upper_bound = np.nan
            return

        smspp_status = res.group(1).replace("\r", "")
        self._status = smspp_status

        res = re.search("Upper bound = (.*)\n", self._log)
        ub = float(res.group(1).replace("\r", ""))

        res = re.search("Lower bound = (.*)\n", self._log)
        lb = float(res.group(1).replace("\r", ""))

        self._objective_value = ub
        self._lower_bound = lb
        self._upper_bound = ub

    @property
    def status(self):
        return self._status

    @property
    def log(self):
        return self._log

    @property
    def objective_value(self):
        return self._objective_value

    @property
    def lower_bound(self):
        return self._lower_bound

    @property
    def upper_bound(self):
        return self._upper_bound

    @property
    def solution(self):
        """
        Returns the solution of the optimization problem.
        This is a placeholder method and should be implemented in derived
        classes if applicable.
        """
        return self._solution

    @property
    def subprocess_time(self):
        """
        Returns the time taken to run the subprocess in seconds.
        This is a placeholder method and should be implemented in derived
        classes if applicable.
        """
        return self._subprocess_time

    @property
    def solution_time(self):
        """
        Returns the time taken to parse the solution in seconds.
        This is a placeholder method and should be implemented in derived
        classes if applicable.
        """
        return self._solution_time

    @property
    def computational_time(self):
        """
        Returns the total computational time of the optimization in seconds.
        This is a placeholder method and should be implemented in derived
        classes if applicable.
        """
        return self._computational_time

computational_time property

Returns the total computational time of the optimization in seconds. This is a placeholder method and should be implemented in derived classes if applicable.

solution property

Returns the solution of the optimization problem. This is a placeholder method and should be implemented in derived classes if applicable.

solution_time property

Returns the time taken to parse the solution in seconds. This is a placeholder method and should be implemented in derived classes if applicable.

subprocess_time property

Returns the time taken to run the subprocess in seconds. This is a placeholder method and should be implemented in derived classes if applicable.

__init__(solver_path, fp_network=None, configfile=None, fp_log=None, fp_solution=None, configsolution=None, help_option='-h', shell=False, **kwargs)

Constructor for an abstract SMSPPSolverTool. Option arguments coincide with the options of the SMS++ solver tools. Additional options can be passed through kwargs. Option "-o" is automatically added to kwargs with value None if not provided, to allow logging the solution.

Parameters:

Name Type Description Default
solver_path str

The name or path of the executable file.

required
fp_network Path | str

Path to the SMSpp network to solve, by default None. When provided, automatically the option "-p" is added to the executable call to specify the folder of the network file.

None
configfile Path | str

Path to the configuration file, by default None. This option specifies the solver configuration with option "-S" when provided. The folder of the configuration file is also specified automatically with option "-c" when provided.

None
fp_log Path | str

When provided, the solver log is saved to the specified log file, by default None.

None
fp_solution Path | str

Path to the solution file, by default None. When provided, option "-O" is added to the executable call to specify the output solution file.

None
configsolution Path | str

Path to the configuration solution file, by default None. When provided, option "-C" is added to the executable call to specify the configuration solution file.

None
help_option str

The option to display the help message, by default "-h".

'-h'
shell bool

Whether to execute the command through the shell. Defaults to False.

False
**kwargs

Additional keyword arguments to pass as options to the function. The keys of the kwargs should be the option name, and the value should be the option value. For example, if the function has an option "-x" that takes a value, the kwargs should include {"x": value}.

{}
Source code in pysmspp/smspp_tools.py
def __init__(
    self,
    solver_path: Path | str,
    fp_network: Path | str = None,
    configfile: Path | str = None,
    fp_log: Path | str = None,
    fp_solution: Path | str = None,
    configsolution: Path | str = None,
    help_option: str = "-h",
    shell: bool = False,
    **kwargs,
):
    """
    Constructor for an abstract SMSPPSolverTool. Option arguments coincide with the options of the SMS++ solver tools. Additional options can be passed through kwargs. Option "-o" is automatically added to kwargs with value None if not provided, to allow logging the solution.

    Parameters
    ----------
    solver_path : str
        The name or path of the executable file.
    fp_network : Path | str, optional
        Path to the SMSpp network to solve, by default None.
        When provided, automatically the option "-p" is added to the executable call to specify the folder of the network file.
    configfile : Path | str, optional
        Path to the configuration file, by default None.
        This option specifies the solver configuration with option "-S" when provided.
        The folder of the configuration file is also specified automatically with option "-c" when provided.
    fp_log : Path | str, optional
        When provided, the solver log is saved to the specified log file, by default None.
    fp_solution : Path | str, optional
        Path to the solution file, by default None.
        When provided, option "-O" is added to the executable call to specify the output solution file.
    configsolution : Path | str, optional
        Path to the configuration solution file, by default None.
        When provided, option "-C" is added to the executable call to specify the configuration solution file.
    help_option : str, optional
        The option to display the help message, by default "-h".
    shell : bool, optional
        Whether to execute the command through the shell. Defaults to False.
    **kwargs
        Additional keyword arguments to pass as options to the function.
        The keys of the kwargs should be the option name, and the value should be the option value.
        For example, if the function has an option "-x" that takes a value, the kwargs should include {"x": value}.
    """
    if isinstance(solver_path, Path):
        self._solver_path = str(solver_path.resolve())
    else:
        self._solver_path = str(solver_path)
    self._help_option = help_option

    self.fp_network = (
        None if fp_network is None else str(Path(fp_network).resolve())
    )
    self.configfile = (
        None if configfile is None else str(Path(configfile).resolve())
    )
    self.configsolution = (
        None if configsolution is None else str(Path(configsolution).resolve())
    )
    self.fp_log = None if fp_log is None else str(Path(fp_log).resolve())
    self.fp_solution = (
        None if fp_solution is None else str(Path(fp_solution).resolve())
    )

    self._shell = shell

    self._status = None
    self._log = None
    self._objective_value = None
    self._lower_bound = None
    self._upper_bound = None
    self._solution = None
    self._subprocess_time = None
    self._solution_time = None
    self._computational_time = None
    self._kwargs = kwargs

    if "c" in self._kwargs:
        raise ValueError(
            "Option 'c' is reserved for the configuration file directory."
        )
    if "p" in self._kwargs:
        raise ValueError("Option 'p' is reserved for the network file directory.")

__repr__()

Return a string representation of the solver tool.

Returns:

Type Description
str

A formatted string showing key solver properties.

Source code in pysmspp/smspp_tools.py
def __repr__(self):
    """
    Return a string representation of the solver tool.

    Returns
    -------
    str
        A formatted string showing key solver properties.
    """
    return f"{type(self).__name__}\n\t\n\tsolver_name={self._solver_path}\n\tstatus={self.status}\n\tconfigfile={self.configfile}\n\tfp_network={self.fp_network}\n\tfp_solution={self.fp_solution}"

calculate_executable_call()

Generate the standard command-line call for SMS++ solvers and can be customized by subclasses.

Returns:

Type Description
list[str]

The command array to execute the solver.

Source code in pysmspp/smspp_tools.py
def calculate_executable_call(self):
    """
    Generate the standard command-line call for SMS++ solvers and can be customized by subclasses.

    Returns
    -------
    list[str]
        The command array to execute the solver.
    """
    if self.configfile is None:
        raise ValueError("configfile must be provided (non-None).")
    if self.fp_network is None:
        raise ValueError("fp_network must be provided (non-None).")
    configdir, configfile = os.path.split(self.configfile)
    networkdir, networkfile = os.path.split(self.fp_network)
    command = [
        self._solver_path,
        networkfile,
        "-S",
        configfile,
    ]
    if len(configdir) > 0:
        command += ["-c", os.path.join(configdir, "")]
    if len(networkdir) > 0:
        command += ["-p", os.path.join(networkdir, "")]
    if self.fp_solution is not None:
        command += ["-O", self.fp_solution]
    if self.configsolution is not None:
        command += ["-C", self.configsolution]

    for option, value in self._kwargs.items():
        if value == "" or value is None:
            command += [f"-{option}"]
        else:
            command += [f"-{option}", str(value)]

    return command

help(print_message=True)

Print the help message of the SMS++ solver tool.

solver.help()

Parameters:

Name Type Description Default
print_message bool

Whether to print the message, by default True.

True

Returns:

Type Description
The help message.
Source code in pysmspp/smspp_tools.py
def help(self, print_message=True):
    """
    Print the help message of the SMS++ solver tool.

    >>> solver.help()

    Parameters
    ----------
    print_message : bool, optional
        Whether to print the message, by default True.

    Returns
    -------
    The help message.
    """
    result = subprocess.run(
        [self._solver_path, self._help_option],
        capture_output=True,
        shell=self._shell,
    )
    msg = result.stdout.decode("utf-8") + os.linesep + result.stderr.decode("utf-8")
    if print_message:
        print(msg)
    return msg

is_available()

Check if the SMS++ tool is available in the PATH.

Parameters:

Name Type Description Default
shell bool

Whether to execute the command through the shell. Defaults to False.

required

Returns:

Type Description
bool

True if the tool is available, False otherwise.

Source code in pysmspp/smspp_tools.py
def is_available(self):
    """
    Check if the SMS++ tool is available in the PATH.

    Parameters
    ----------
    shell : bool, optional
        Whether to execute the command through the shell. Defaults to False.

    Returns
    -------
    bool
        True if the tool is available, False otherwise.
    """
    try:
        proc = subprocess.run(
            [self._solver_path, self._help_option],
            check=False,
            shell=self._shell,
        )
        return proc.returncode == 0
    except FileNotFoundError:
        return False
    except Exception as e:
        logger.warning(f"Error checking availability of {self._solver_path}: {e}")
        return False

optimize(logging=True, tracking_period=0.1)

Run the SMSPP Solver tool.

Parameters:

Name Type Description Default
logging bool

When true, logging is provided, including the executable call.

True
tracking_period float

Delay in seconds between resource usage tracking samples.

0.1
Source code in pysmspp/smspp_tools.py
def optimize(self, logging=True, tracking_period=0.1):
    """
    Run the SMSPP Solver tool.

    Parameters
    ----------
    logging : bool
        When true, logging is provided, including the executable call.
    tracking_period : float
        Delay in seconds between resource usage tracking samples.
    """
    from pysmspp import SMSNetwork

    if not Path(self.configfile).exists():
        raise FileNotFoundError(
            f"Configuration file {self.configfile} does not exist."
        )
    if not Path(self.fp_network).exists():
        raise FileNotFoundError(f"Network file {self.fp_network} does not exist.")

    command_raw = self.calculate_executable_call()
    command_str = " ".join(command_raw)
    command = command_raw if not self._shell else command_str

    start_time = time.time()
    if logging:
        print(f"Executing command:\n{command_str}\n")

    process = psutil.Popen(
        command,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        shell=self._shell,
    )
    pipe_messages = queue.Queue()
    stdout_thread = threading.Thread(
        target=_enqueue_pipe_lines,
        args=(process.stdout, "stdout", pipe_messages),
        daemon=True,
    )
    stderr_thread = threading.Thread(
        target=_enqueue_pipe_lines,
        args=(process.stderr, "stderr", pipe_messages),
        daemon=True,
    )
    stdout_thread.start()
    stderr_thread.start()
    process.cpu_percent()  # initialize cpu percent calculation

    self._log = ""
    log_error = ""

    peak_memory = 0
    peak_cpu = 0

    loop = True
    while loop:
        if process.poll() is not None:
            loop = False
        else:
            try:
                # capture resource usage when process is active
                mem = process.memory_info().rss
                cpu = process.cpu_percent()

                # track the peak utilization of the process
                if mem > peak_memory:
                    peak_memory = mem
                if cpu > peak_cpu:
                    peak_cpu = cpu
            except psutil.NoSuchProcess:
                pass

        # read from process without stopping it
        msg_out, msg_err = _drain_pipe_messages(pipe_messages, logging)

        self._log += msg_out + msg_err
        log_error += msg_err

        time.sleep(tracking_period)

    # finalize logging
    stdout_thread.join()
    stderr_thread.join()
    msg_out, msg_err = _drain_pipe_messages(pipe_messages, logging)

    self._log += msg_out + msg_err
    log_error += msg_err

    # add memory info to logging
    self._subprocess_time = time.time() - start_time

    msg = f"Peak CPU Usage: {peak_cpu:.2f} %"
    msg += f"\nPeak Memory Usage: {peak_memory / (1024**2):.2f} MB"
    msg += f"\nTotal Time: {self._subprocess_time:.2f} seconds\n"

    self._log += msg
    if logging:
        print(msg)

    self.parse_solver_log()

    if process.returncode != 0:
        raise ValueError(
            f"Failed to run {self._solver_path} with error log:\n{log_error}\n\nFull log:\n{self._log}"
        )

    # write output to file, if option passed
    if self.fp_log is not None:
        Path(self.fp_log).parent.mkdir(parents=True, exist_ok=True)
        with open(self.fp_log, "w") as f:
            f.write(self._log)

    # sets the solution object
    start_solution_time = time.time()
    if self.fp_solution is not None:
        if Path(self.fp_solution).exists():
            self._solution = SMSNetwork(self.fp_solution)
        else:
            raise FileNotFoundError(
                f"solution file {self.fp_solution} does not exist."
            )
    else:
        self._solution = None
    self._solution_time = time.time() - start_solution_time
    self._computational_time = time.time() - start_time

    return self

parse_solver_log()

Check the output of the Solver. It will extract the status, upper bound, lower bound, and objective value from the log.

Parameters:

Name Type Description Default
log str

The path to the solution file.

required
Source code in pysmspp/smspp_tools.py
def parse_solver_log(self):
    """
    Check the output of the Solver.
    It will extract the status, upper bound, lower bound, and objective value from the log.

    Parameters
    ----------
    log : str
        The path to the solution file.
    """
    if self._log is None:
        raise ValueError("Optimization was not launched.")

    res = re.search("Status = (.*)\n", self._log)

    if not res:  # if success not found
        self._status = "Failed"
        self._objective_value = np.nan
        self._lower_bound = np.nan
        self._upper_bound = np.nan
        return

    smspp_status = res.group(1).replace("\r", "")
    self._status = smspp_status

    res = re.search("Upper bound = (.*)\n", self._log)
    ub = float(res.group(1).replace("\r", ""))

    res = re.search("Lower bound = (.*)\n", self._log)
    lb = float(res.group(1).replace("\r", ""))

    self._objective_value = ub
    self._lower_bound = lb
    self._upper_bound = ub

TSSBSolver

Bases: SMSPPSolverTool

Class to interact with the TSSBSolver tool from SMS++, with name "tssb_solver".

Source code in pysmspp/smspp_tools.py
class TSSBSolver(SMSPPSolverTool):
    """
    Class to interact with the TSSBSolver tool from SMS++, with name "tssb_solver".
    """

    def __init__(
        self,
        solver_path: Path | str = "tssb_solver",
        fp_network: Path | str = None,
        configfile: Path | str = None,
        fp_log: Path | str = None,
        fp_solution: Path | str = None,
        configsolution: Path | str = None,
        help_option: str = "-h",
        **kwargs,
    ):
        """
        The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.
        """
        super().__init__(
            solver_path=solver_path,
            fp_network=fp_network,
            configfile=configfile,
            fp_log=fp_log,
            fp_solution=fp_solution,
            configsolution=configsolution,
            help_option=help_option,
            **kwargs,
        )

__init__(solver_path='tssb_solver', fp_network=None, configfile=None, fp_log=None, fp_solution=None, configsolution=None, help_option='-h', **kwargs)

The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.

Source code in pysmspp/smspp_tools.py
def __init__(
    self,
    solver_path: Path | str = "tssb_solver",
    fp_network: Path | str = None,
    configfile: Path | str = None,
    fp_log: Path | str = None,
    fp_solution: Path | str = None,
    configsolution: Path | str = None,
    help_option: str = "-h",
    **kwargs,
):
    """
    The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.
    """
    super().__init__(
        solver_path=solver_path,
        fp_network=fp_network,
        configfile=configfile,
        fp_log=fp_log,
        fp_solution=fp_solution,
        configsolution=configsolution,
        help_option=help_option,
        **kwargs,
    )

UCBlockSolver

Bases: SMSPPSolverTool

Class to interact with the UCBlockSolver tool from SMS++.

Source code in pysmspp/smspp_tools.py
class UCBlockSolver(SMSPPSolverTool):
    """
    Class to interact with the UCBlockSolver tool from SMS++.
    """

    def __init__(
        self,
        solver_path: Path | str = "ucblock_solver",
        fp_network: Path | str = None,
        configfile: Path | str = None,
        fp_log: Path | str = None,
        fp_solution: Path | str = None,
        configsolution: Path | str = None,
        help_option: str = "-h",
        **kwargs,
    ):
        """
        The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.
        """
        super().__init__(
            solver_path=solver_path,
            fp_network=fp_network,
            configfile=configfile,
            fp_log=fp_log,
            fp_solution=fp_solution,
            configsolution=configsolution,
            help_option=help_option,
            **kwargs,
        )

__init__(solver_path='ucblock_solver', fp_network=None, configfile=None, fp_log=None, fp_solution=None, configsolution=None, help_option='-h', **kwargs)

The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.

Source code in pysmspp/smspp_tools.py
def __init__(
    self,
    solver_path: Path | str = "ucblock_solver",
    fp_network: Path | str = None,
    configfile: Path | str = None,
    fp_log: Path | str = None,
    fp_solution: Path | str = None,
    configsolution: Path | str = None,
    help_option: str = "-h",
    **kwargs,
):
    """
    The arguments of the constructor coincide with the options of SMSPPSolverTool; see the base class for details.
    """
    super().__init__(
        solver_path=solver_path,
        fp_network=fp_network,
        configfile=configfile,
        fp_log=fp_log,
        fp_solution=fp_solution,
        configsolution=configsolution,
        help_option=help_option,
        **kwargs,
    )

is_smspp_installed(solvers=[UCBlockSolver()])

Check if SMS++ is installed by verifying that the specified solver executables can be found in the PATH.

Parameters:

Name Type Description Default
solvers list[type[SMSPPSolverTool]]

List of solver classes to check. Defaults to [UCBlockSolver]. Available solvers: UCBlockSolver, InvestmentBlockTestSolver, InvestmentBlockSolver, SDDPSolver.

[UCBlockSolver()]

Returns:

Type Description
bool

True if all specified SMS++ solvers are installed, False otherwise.

Examples:

>>> import pysmspp
>>> if pysmspp.is_smspp_installed():
...     print("SMS++ is installed and available")
... else:
...     print("SMS++ is not available")
>>> # Check multiple solvers
>>> if pysmspp.is_smspp_installed([pysmspp.UCBlockSolver, pysmspp.InvestmentBlockTestSolver]):
...     print("Both solvers are available")
Source code in pysmspp/smspp_tools.py
def is_smspp_installed(solvers: list[SMSPPSolverTool] = [UCBlockSolver()]) -> bool:
    """
    Check if SMS++ is installed by verifying that the specified solver executables
    can be found in the PATH.

    Parameters
    ----------
    solvers : list[type[SMSPPSolverTool]], optional
        List of solver classes to check. Defaults to [UCBlockSolver].
        Available solvers: UCBlockSolver, InvestmentBlockTestSolver,
        InvestmentBlockSolver, SDDPSolver.

    Returns
    -------
    bool
        True if all specified SMS++ solvers are installed, False otherwise.

    Examples
    --------
    >>> import pysmspp
    >>> if pysmspp.is_smspp_installed():
    ...     print("SMS++ is installed and available")
    ... else:
    ...     print("SMS++ is not available")

    >>> # Check multiple solvers
    >>> if pysmspp.is_smspp_installed([pysmspp.UCBlockSolver, pysmspp.InvestmentBlockTestSolver]):
    ...     print("Both solvers are available")
    """
    # Check if all specified solvers are available
    return all(solver.is_available() for solver in solvers)

components

Dict

Bases: dict

Dict is a subclass of dict, which allows you to get AND SET items in the dict using the attribute syntax!

Imported from https://github.com/PyPSA/pypsa, derived from addict https://github.com/mewwts/addict/ .

Source code in pysmspp/components.py
class Dict(dict):
    """
    Dict is a subclass of dict, which allows you to get AND SET items in the
    dict using the attribute syntax!

    Imported from https://github.com/PyPSA/pypsa, derived from addict https://github.com/mewwts/addict/ .
    """

    def __setattr__(self, name: str, value: Any) -> None:
        """
        Setattr is called when the syntax a.b = 2 is used to set a value.
        """
        if hasattr(Dict, name):
            raise AttributeError(f"'Dict' object attribute '{name}' is read-only")
        self[name] = value

    def __getattr__(self, item: str) -> Any:
        """
        Get an item using attribute syntax (e.g., dict.key).

        Parameters
        ----------
        item : str
            The key name to retrieve.

        Returns
        -------
        Any
            The value associated with the key.

        Raises
        ------
        AttributeError
            If the key does not exist.
        """
        try:
            return self.__getitem__(item)
        except KeyError as e:
            raise AttributeError(e.args[0])

    def __delattr__(self, name: str) -> None:
        """
        Is invoked when del some_addict.b is called.
        """
        del self[name]

    _re_pattern = re.compile("[a-zA-Z_][a-zA-Z0-9_]*")

    def __dir__(self) -> list[str]:
        """
        Return a list of object attributes.

        This includes key names of any dict entries, filtered to the
        subset of valid attribute names (e.g. alphanumeric strings
        beginning with a letter or underscore).  Also includes
        attributes of parent dict class.
        """
        dict_keys = []
        for k in self.keys():
            if isinstance(k, str):
                if m := self._re_pattern.match(k):
                    dict_keys.append(m.string)

        obj_attrs = list(dir(Dict))

        return dict_keys + obj_attrs

__delattr__(name)

Is invoked when del some_addict.b is called.

Source code in pysmspp/components.py
def __delattr__(self, name: str) -> None:
    """
    Is invoked when del some_addict.b is called.
    """
    del self[name]

__dir__()

Return a list of object attributes.

This includes key names of any dict entries, filtered to the subset of valid attribute names (e.g. alphanumeric strings beginning with a letter or underscore). Also includes attributes of parent dict class.

Source code in pysmspp/components.py
def __dir__(self) -> list[str]:
    """
    Return a list of object attributes.

    This includes key names of any dict entries, filtered to the
    subset of valid attribute names (e.g. alphanumeric strings
    beginning with a letter or underscore).  Also includes
    attributes of parent dict class.
    """
    dict_keys = []
    for k in self.keys():
        if isinstance(k, str):
            if m := self._re_pattern.match(k):
                dict_keys.append(m.string)

    obj_attrs = list(dir(Dict))

    return dict_keys + obj_attrs

__getattr__(item)

Get an item using attribute syntax (e.g., dict.key).

Parameters:

Name Type Description Default
item str

The key name to retrieve.

required

Returns:

Type Description
Any

The value associated with the key.

Raises:

Type Description
AttributeError

If the key does not exist.

Source code in pysmspp/components.py
def __getattr__(self, item: str) -> Any:
    """
    Get an item using attribute syntax (e.g., dict.key).

    Parameters
    ----------
    item : str
        The key name to retrieve.

    Returns
    -------
    Any
        The value associated with the key.

    Raises
    ------
    AttributeError
        If the key does not exist.
    """
    try:
        return self.__getitem__(item)
    except KeyError as e:
        raise AttributeError(e.args[0])

__setattr__(name, value)

Setattr is called when the syntax a.b = 2 is used to set a value.

Source code in pysmspp/components.py
def __setattr__(self, name: str, value: Any) -> None:
    """
    Setattr is called when the syntax a.b = 2 is used to set a value.
    """
    if hasattr(Dict, name):
        raise AttributeError(f"'Dict' object attribute '{name}' is read-only")
    self[name] = value