Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ optional solver dependency because it is difficult to build on common pip-only
environments, is not published on PyPI above version 3.0, and is only needed for
the solver-backed unit-operation models. Use the conda-forge environment in
`environment.yml`, or a local source build of Assimulo, for solver-backed runs.
Unit-operation modules and their solver-free calculations remain importable
without Assimulo. Attempting to construct an Assimulo-backed problem or solver
without that optional dependency raises an installation error at the solver
boundary.

Current bounds:

Expand Down
3 changes: 1 addition & 2 deletions PharmaPy/Containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
@author: dcasasor
"""

from assimulo.solvers import CVode
from assimulo.problem import Explicit_Problem
from PharmaPy._assimulo import CVode, Explicit_Problem

from PharmaPy.Phases import LiquidPhase, SolidPhase, classify_phases
from PharmaPy.Streams import LiquidStream, SolidStream
Expand Down
3 changes: 1 addition & 2 deletions PharmaPy/Crystallizers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
# import autograd.numpy as np


from assimulo.solvers import CVode
from assimulo.problem import Explicit_Problem
from PharmaPy._assimulo import CVode, Explicit_Problem

from PharmaPy.Phases import classify_phases
from PharmaPy.Streams import LiquidStream, SolidStream
Expand Down
4 changes: 1 addition & 3 deletions PharmaPy/Distillation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import numpy as np
from assimulo.problem import Implicit_Problem
from PharmaPy._assimulo import IDA, Implicit_Problem
from PharmaPy.Phases import classify_phases
from PharmaPy.Streams import VaporStream
from PharmaPy.Connections import get_inputs_new
Expand All @@ -10,8 +10,6 @@
from PharmaPy.Plotting import plot_distrib
from PharmaPy.CheckModule import check_modeling_objects

from assimulo.solvers import IDA

import scipy.optimize
import scipy.sparse

Expand Down
3 changes: 1 addition & 2 deletions PharmaPy/Drying_Model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
"""
import numpy as np
import numpy.matlib
from assimulo.problem import Explicit_Problem
from assimulo.solvers import CVode
from PharmaPy._assimulo import CVode, Explicit_Problem
import matplotlib.pyplot as plt
import scipy
from scipy.interpolate import CubicSpline
Expand Down
5 changes: 1 addition & 4 deletions PharmaPy/DynamicExtraction.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import numpy as np
from scipy import linalg
from scipy.optimize import root
from assimulo.problem import Implicit_Problem
from PharmaPy._assimulo import IDA, Implicit_Problem

from PharmaPy.Phases import classify_phases
from PharmaPy.Connections import get_inputs_new
Expand All @@ -12,9 +12,6 @@
from PharmaPy.Extractors import BatchExtractor, validate_gamma_method
from PharmaPy.Plotting import plot_distrib

from assimulo.solvers import IDA
from assimulo.solvers import Radau5DAE

import copy

from matplotlib.ticker import MaxNLocator, AutoMinorLocator
Expand Down
8 changes: 3 additions & 5 deletions PharmaPy/Evaporators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
# from autograd import numpy as np
# from autograd import jacobian as jacauto
from PharmaPy.Commons import (mid_fn, trapezoidal_rule, eval_state_events,
handle_events, unpack_states, flatten_states)
handle_events, unpack_states, flatten_states,
TerminateSimulation)
from PharmaPy._assimulo import IDA, Implicit_Problem
from PharmaPy.Connections import get_inputs, get_inputs_new
from PharmaPy.Streams import LiquidStream, VaporStream
from PharmaPy.Phases import LiquidPhase, VaporPhase, classify_phases
Expand All @@ -17,10 +19,6 @@
import matplotlib.pyplot as plt
from matplotlib.ticker import AutoMinorLocator

from assimulo.problem import Implicit_Problem
from assimulo.solvers import IDA
from assimulo.exception import TerminateSimulation

from pathlib import Path
import copy

Expand Down
3 changes: 1 addition & 2 deletions PharmaPy/Reactors.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-


from assimulo.solvers import CVode, LSODAR
from assimulo.problem import Explicit_Problem
from PharmaPy._assimulo import CVode, Explicit_Problem

from PharmaPy.Phases import classify_phases
from PharmaPy.Commons import (reorder_sens, plot_sens, trapezoidal_rule,
Expand Down
9 changes: 4 additions & 5 deletions PharmaPy/SolidLiquidSep.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
"""

import numpy as np
from PharmaPy._assimulo import CVode, Explicit_Problem
from PharmaPy.Commons import trapezoidal_rule, series_erfc
from PharmaPy.Phases import classify_phases
from PharmaPy.MixedPhases import Slurry, Cake
from PharmaPy.general_interpolation import define_initial_state

from PharmaPy.Commons import unpack_states, reorder_pde_outputs, eval_state_events, handle_events, unpack_discretized
from PharmaPy.Commons import (unpack_states, reorder_pde_outputs,
eval_state_events, handle_events,
unpack_discretized, TerminateSimulation)
from PharmaPy.Connections import get_inputs_new
from PharmaPy.Results import DynamicResult
from PharmaPy.NameAnalysis import get_dict_states
Expand All @@ -22,10 +25,6 @@
from matplotlib.animation import FFMpegWriter
import copy

from assimulo.solvers import CVode
from assimulo.problem import Explicit_Problem
from assimulo.exception import TerminateSimulation

from scipy.special import erfc

eps = np.finfo(float).eps * 1.1
Expand Down
4 changes: 1 addition & 3 deletions PharmaPy/ThreePhaseSettler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@
"""

import numpy as np
from assimulo.problem import Implicit_Problem
from PharmaPy._assimulo import IDA, Implicit_Problem
from PharmaPy.Phases import classify_phases
from PharmaPy.Connections import get_inputs_new
from PharmaPy.Streams import LiquidStream, SolidStream
from PharmaPy.MixedPhases import Slurry, SlurryStream
from PharmaPy.Results import DynamicResult
from PharmaPy.Plotting import plot_distrib

from assimulo.solvers import IDA

import scipy.optimize
import scipy.sparse

Expand Down
182 changes: 182 additions & 0 deletions PharmaPy/_assimulo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
"""Lazy internal constructors for the optional Assimulo solver backend.

The exported names are factory functions, not Assimulo class aliases, and
therefore are not suitable for ``isinstance`` checks or subclassing.
"""

from importlib import import_module
from typing import Any

_INSTALL_MESSAGE = (
"Assimulo is required for solver-backed PharmaPy simulations. "
"Install Assimulo 3.4.3 from conda-forge using environment.yml or "
"provide a compatible local installation."
)


def _load_assimulo_symbol(module_name: str, symbol_name: str) -> Any:
"""Load a symbol from the optional Assimulo dependency.

Parameters
----------
module_name : str
Fully qualified Assimulo module name.
symbol_name : str
Attribute to load from the module.

Returns
-------
Any
Requested Assimulo class or callable.

Raises
------
ImportError
If Assimulo or one of its required runtime components cannot be
imported.
"""
try:
module = import_module(module_name)
except ImportError as exc:
raise ImportError(_INSTALL_MESSAGE) from exc

try:
return getattr(module, symbol_name)
except AttributeError as exc:
raise ImportError(
f"{_INSTALL_MESSAGE} The installed version does not provide "
f"{module_name}.{symbol_name}."
) from exc


def _construct_assimulo_object(
module_name: str,
symbol_name: str,
*args: Any,
**kwargs: Any,
) -> Any:
"""Construct an object from the optional Assimulo dependency.

Parameters
----------
module_name : str
Fully qualified Assimulo module name.
symbol_name : str
Constructor name within ``module_name``.
*args : Any
Positional arguments forwarded unchanged to the constructor.
**kwargs : Any
Keyword arguments forwarded unchanged to the constructor.

Returns
-------
Any
Constructed Assimulo problem or solver object.

Raises
------
ImportError
If the requested Assimulo constructor is unavailable.
"""
constructor = _load_assimulo_symbol(module_name, symbol_name)
return constructor(*args, **kwargs)


def CVode(*args: Any, **kwargs: Any) -> Any:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nonblocking: these four public symbols (CVode, IDA, Explicit_Problem, Implicit_Problem) are named like the Assimulo classes but are factory functions returning instances. That is deliberate and correct for the current call sites (all constructor calls), but a future contributor porting Assimulo code could write isinstance(solver, CVode) or subclass Explicit_Problem and it would fail silently/oddly since these are functions, not the classes. Consider a one-line module note (or comment on each) stating they are lazy constructors, not the classes, so nobody reaches for isinstance/subclassing. Not a blocker.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ccd161c. The module docstring now states that these exports are
factory functions rather than Assimulo class aliases and warns that they are
unsuitable for isinstance checks or subclassing.

"""Construct an Assimulo CVode solver on first use.

Parameters
----------
*args : Any
Positional arguments forwarded to ``assimulo.solvers.CVode``.
**kwargs : Any
Keyword arguments forwarded to ``assimulo.solvers.CVode``.

Returns
-------
Any
Assimulo CVode solver instance.

Raises
------
ImportError
If the optional Assimulo backend is unavailable.
"""
return _construct_assimulo_object("assimulo.solvers", "CVode", *args, **kwargs)


def IDA(*args: Any, **kwargs: Any) -> Any:
"""Construct an Assimulo IDA solver on first use.

Parameters
----------
*args : Any
Positional arguments forwarded to ``assimulo.solvers.IDA``.
**kwargs : Any
Keyword arguments forwarded to ``assimulo.solvers.IDA``.

Returns
-------
Any
Assimulo IDA solver instance.

Raises
------
ImportError
If the optional Assimulo backend is unavailable.
"""
return _construct_assimulo_object("assimulo.solvers", "IDA", *args, **kwargs)


def Explicit_Problem(*args: Any, **kwargs: Any) -> Any:
"""Construct an Assimulo explicit problem on first use.

Parameters
----------
*args : Any
Positional arguments forwarded to
``assimulo.problem.Explicit_Problem``.
**kwargs : Any
Keyword arguments forwarded to
``assimulo.problem.Explicit_Problem``.

Returns
-------
Any
Assimulo explicit-problem instance.

Raises
------
ImportError
If the optional Assimulo backend is unavailable.
"""
return _construct_assimulo_object(
"assimulo.problem", "Explicit_Problem", *args, **kwargs
)


def Implicit_Problem(*args: Any, **kwargs: Any) -> Any:
"""Construct an Assimulo implicit problem on first use.

Parameters
----------
*args : Any
Positional arguments forwarded to
``assimulo.problem.Implicit_Problem``.
**kwargs : Any
Keyword arguments forwarded to
``assimulo.problem.Implicit_Problem``.

Returns
-------
Any
Assimulo implicit-problem instance.

Raises
------
ImportError
If the optional Assimulo backend is unavailable.
"""
return _construct_assimulo_object(
"assimulo.problem", "Implicit_Problem", *args, **kwargs
)
Loading