Skip to content

Commit

Permalink
Merge pull request #61 from sstroemer/feature-writefile-iis
Browse files Browse the repository at this point in the history
feat: expose `write_to_file` and `compute_iis`, including fixes for manual Julia package installation
  • Loading branch information
sstroemer authored Jan 10, 2025
2 parents da19e82 + 35d0014 commit ec53075
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ cython_debug/
# Prevent tracking any JuliaPkg files
**/juliapkg.json

# Prevent tracking root folder Python files
*.py

# Temp. directory
tmp_test/
**/tmp_test/
15 changes: 15 additions & 0 deletions docs/pages/manual/python/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ IESOPT_SOLVER_HIGHS = 1.12.0

Other examples may be setting `IESOPT_SOLVER_CPLEX` or `IESOPT_SOLVER_GUROBI`.

### Forcing a solver executable version

For example when installing Gurobi, it might be that the Julia wrapper pulls a version newer than the one you are able
to use according to your license. This can be fixed by explicitly adding the corresponding `_jll` ("binary wrapper")
as dependency:

```{code-block} text
:caption: Fixing a solver executable version in `.env`.
IESOPT_PKG_Gurobi_jll = 11.0.3
IESOPT_SOLVER_GUROBI = 1.6.0
```

This will use the latest `Gurobi_jll` version related to Gurobi 11.

## Important versions

The following other entries are potentially used in some projects:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "iesopt"
version = "2.2.1"
version = "2.3.0"
description = "IESopt -- an Integrated Energy System Optimization framework."
keywords = ["integrated energy systems", "optimization", "energy model", "modelling"]
authors = [
Expand Down
8 changes: 4 additions & 4 deletions src/iesopt/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ def _strtobool(val: str):
class Config:
DEFAULTS = {
"IESOPT_JULIA": "1.11.2",
"IESOPT_CORE": "2.2.0",
"IESOPT_JUMP": "1.23.5",
"IESOPT_SOLVER_HIGHS": "1.12.2",
"IESOPT_CORE": "2.3.0",
"IESOPT_JUMP": "1.23.6",
"IESOPT_SOLVER_HIGHS": "1.13.0",
"IESOPT_MULTITHREADED": "no", # yes, no
"IESOPT_OPTIMIZATION": "latency", # rapid, latency, normal, performance
}
Expand All @@ -27,7 +27,7 @@ def init(cls):
return

cls._config = {
k[7:].lower(): v
(k[7:] if k.startswith("IESOPT_PKG_") else k[7:].lower()): v
for (k, v) in {
**cls.DEFAULTS,
**dotenv_values(),
Expand Down
16 changes: 12 additions & 4 deletions src/iesopt/julia/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# import ssl

from ..util import logger
from .util import jl_import, jl_safe_seval
from .util import jl_import
from ..config import Config


Expand Down Expand Up @@ -122,13 +122,21 @@ def setup_julia():
logger.info(" Executable: %s" % juliapkg.executable())
logger.info(" Project: %s" % juliapkg.project())

custom_packages = list(Config.find("pkg_"))
custom_packages = list(Config.find("PKG_"))
if len(custom_packages) > 0:
logger.info("Installing custom Julia packages")
jl_import("Pkg")

try:
juliacall.Main.seval("import Pkg")
except Exception as e:
logger.error(f"Failed to import Julia `Pkg`: {e}")

for entry in custom_packages:
name = entry[4:]
jl_safe_seval(f"Pkg.add(Pkg.{name})")
try:
juliacall.Pkg.add(name=name, version=Config.get(entry))
except Exception as e:
logger.error(f"Failed to install custom package '{name}': {e}")

# # Restoring potential SSL certificate.
# if _ssl is not None:
Expand Down
6 changes: 5 additions & 1 deletion src/iesopt/julia/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def jl_safe_seval(code: str):
return get_iesopt_module_attr("julia").seval(code)
except Exception as e:
logger.error("Exception while trying to execute Julia code: `%s`" % code)
logger.error("Exception: `%s`" % str(e))
logger.error("Exception details: `%s`" % getattr(e, "exception", "missing"))

return None
Expand Down Expand Up @@ -57,7 +58,10 @@ def jl_isa(obj, julia_type: str):

def jl_docs(obj: str, module: str = "IESopt"):
"""Get the documentation string of a Julia object inside the IESopt module."""
return "".join(el for el in jl_safe_seval(f"@doc {module}.{obj}").text)
julia_docstr = jl_safe_seval(f"@doc {module}.{obj}")
if julia_docstr is None:
return "Missing documentation string."
return "".join(el for el in julia_docstr.text)


def recursive_convert_py2jl(item):
Expand Down
86 changes: 85 additions & 1 deletion src/iesopt/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from warnings import warn

from .util import logger, get_iesopt_module_attr
from .julia.util import jl_symbol, recursive_convert_py2jl
from .julia.util import jl_symbol, recursive_convert_py2jl, jl_safe_seval
from .results import Results


Expand Down Expand Up @@ -111,6 +111,58 @@ def generate(self) -> None:
except Exception as e:
logger.error("Failed to extract debugging info")

def write_to_file(self, filename=None, *, format: str = "automatic") -> str:
"""Write the model to a file.
Consult the Julia version of this function, [IESopt.write_to_file](https://ait-energy.github.io/iesopt/pages/manual/julia/index.html#write-to-file)
for more information. This will automatically invoke `generate` if the model has not been generated yet.
Arguments:
filename (Optional[str]): The filename to write to. If `None`, the path and name are automatically
determined by IESopt.
Keyword Arguments:
format (str): The format to write the file in. If `automatic`, the format is determined based on the
extension of the filename. Otherwise, it used as input to [`JuMP.write_to_file`](https://jump.dev/JuMP.jl/stable/api/JuMP/#write_to_file),
by converting to uppercase and prefixing it with `MOI.FileFormats.FORMAT_`. Writing to, e.g.,
an LP file can be done by setting `format="lp"` or `format="LP"`.
Returns:
str: The filename (including path) of the written file.
Examples:
.. code-block:: python
:caption: Writing a model to a problem file.
import iesopt
cfg = iesopt.make_example("01_basic_single_node", dst_dir="opt")
# Model will be automatically generated when calling `write_to_file`:
model = iesopt.Model(cfg)
model.write_to_file()
# It also works with already optimized models:
model = iesopt.run(cfg)
model.write_to_file("opt/out/my_problem.LP")
# And supports different formats:
target = model.write_to_file("opt/out/my_problem.foo", format="mof")
print(target)
"""
if self._status == ModelStatus.EMPTY:
self.generate()

try:
if filename is None:
return self._IESopt.write_to_file(self.core)
else:
format = jl_safe_seval(f"JuMP.MOI.FileFormats.FORMAT_{format.upper()}")
return self._IESopt.write_to_file(self.core, str(filename), format=format)
except Exception as e:
logger.error(f"Error while writing model to file: {e}")
return ""

def optimize(self) -> None:
"""Optimize the model."""
try:
Expand All @@ -126,6 +178,9 @@ def optimize(self) -> None:
_term_status = self._JuMP.termination_status(self.core)
if str(_term_status) == "INFEASIBLE":
self._status = ModelStatus.INFEASIBLE
logger.error(
"The model seems to be infeasible; refer to `model.compute_iis()` and its documentation if you want to know more about the source of the infeasibility."
)
elif str(_term_status) == "INFEASIBLE_OR_UNBOUNDED":
self._status = ModelStatus.INFEASIBLE_OR_UNBOUNDED
else:
Expand All @@ -139,6 +194,35 @@ def optimize(self) -> None:
except Exception as e:
logger.error("Failed to extract debugging info")

def compute_iis(self, filename=None) -> None:
"""Compute and print the Irreducible Infeasible Set (IIS) of the model, or optionally write it to a file.
Note that this requires a solver that supports IIS computation.
Arguments:
filename (Optional[str | Path]): The filename to write the IIS to. If `None`, the IIS is only printed to the
console.
Examples:
.. code-block:: python
:caption: Computing the IIS of a model.
import iesopt
model = iesopt.run("infeasible_model.iesopt.yaml")
model.compute_iis()
# or (arbitrary filename/extension):
model.compute_iis(filename="my_problem.txt")
"""
try:
if filename is None:
self._IESopt.compute_IIS(self.core)
else:
self._IESopt.compute_IIS(self.core, filename=str(filename))
except Exception as e:
logger.error("Error while computing IIS: `%s`" % str(e.args[0]))

@property
def results(self) -> Results:
"""Get the results of the model."""
Expand Down

0 comments on commit ec53075

Please sign in to comment.