Repository: PyPSA/PyPSA

Issue:
After creating the optimization model, copying the model throws an error
### Version Checks (indicate both or one)

- [x] I have confirmed this bug exists on the lastest [release](https://github.com/pypsa/pypsa/releases) of PyPSA.

- [x] I have confirmed this bug exists on the current [`master`](https://github.com/pypsa/pypsa/tree/master) branch of PyPSA.


### Issue Description

When executing the network.optimize.create_model(), copying the network throws an error. The code is searching for a property solver_model which doesn't seem to exist in this case yet.

### Reproducible Example

```python
import pypsa

network = pypsa.examples.ac_dc_meshed(from_master=True)
m = network.optimize.create_model()
n_copy = network.copy()
```

### Expected Behavior

  File "C:\Users\mbui\Desktop\git\PyPSA-AtlanticLoop-Model\minimal_example_network.py", line 93, in <module>
    n_copy = network.copy()
             ^^^^^^^^^^^^^^
  File "C:\Users\mbui\.venv\pypsa_cad_p312\Lib\site-packages\pypsa\networks.py", line 749, in copy
    if self._model is not None and self._model.solver_model is not None:
                                   ^^^^^^^^^^^^^^^^^^^^^^^^ 
AttributeError: 'Model' object has no attribute 'solver_model'. Did you mean: 'solver_dir'?

### Installed Versions

Python 3.11 and 3.12
aiohappyeyeballs==2.6.1
aiohttp==3.12.13
aiosignal==1.3.2
alabaster==1.0.0
annotated-types==0.7.0
ansicolors==1.1.8
anyio==4.9.0
argon2-cffi==25.1.0
argon2-cffi-bindings==21.2.0
arrow==1.3.0
asttokens==3.0.0
attrs==25.3.0
autopep8==2.3.2
babel==2.17.0
backoff==2.2.1
beautifulsoup4==4.13.4
bleach==6.2.0
blosc2==3.5.0
Bottleneck==1.5.0
branca==0.8.1
Cartopy==0.24.1
certifi==2025.6.15
cffi==1.17.1
cfgv==3.4.0
cftime==1.6.4.post1
charset-normalizer==3.4.2
click==8.2.1
cloudpickle==3.1.1
colorama==0.4.6
comm==0.2.2
contourpy==1.3.2
cycler==0.12.1
dask==2025.5.1
debuglater==1.4.4
debugpy==1.8.14
decorator==5.2.1
defusedxml==0.7.1
deprecation==2.1.0
distlib==0.3.9
distro==1.9.0
docutils==0.21.2
entrypoints==0.4
et_xmlfile==2.0.0
executing==2.2.0
fastjsonschema==2.21.1
filelock==3.18.0
flexcache==0.3
flexparser==0.4
folium==0.20.0
fonttools==4.58.4
fqdn==1.5.1
frozenlist==1.7.0
fsspec==2025.5.1
geopandas==1.1.0
greenlet==3.2.3
highspy==1.11.0
humanize==4.12.3
identify==2.6.12
idna==3.10
imagesize==1.4.1
iniconfig==2.1.0
ipdb==0.13.13
ipykernel==6.29.5
ipython==9.3.0
ipython-genutils==0.2.0
ipython_pygments_lexers==1.1.1
isoduration==20.11.0
jedi==0.19.2
Jinja2==3.1.6
joblib==1.5.1
jsonpointer==3.0.0
jsonschema==4.24.0
jsonschema-specifications==2025.4.1
jupyter-events==0.12.0
jupyter_client==7.4.9
jupyter_core==5.8.1
jupyter_server==2.16.0
jupyter_server_terminals==0.5.3
jupyterlab_pygments==0.3.0
jupytext==1.17.2
kiwisolver==1.4.8
linopy==0.5.5
locket==1.0.0
mapclassify==2.9.0
markdown-it-py==3.0.0
MarkupSafe==3.0.2
matplotlib==3.10.3
matplotlib-inline==0.1.7
mdit-py-plugins==0.4.2
mdurl==0.1.2
mistune==3.1.3
msgpack==1.1.1
multidict==6.5.1
narwhals==1.44.0
nbclassic==1.3.1
nbclient==0.10.2
nbconvert==7.16.6
nbformat==5.10.4
ndindex==1.10.0
nest-asyncio==1.6.0
netCDF4==1.7.2
networkx==3.5
nodeenv==1.9.1
notebook==6.5.7
notebook_shim==0.2.4
numexpr==2.11.0
numpy==1.26.4
openpyxl==3.1.5
overrides==7.7.0
packaging==25.0
pandas==2.3.0
pandocfilters==1.5.1
papermill==2.6.0
parso==0.8.4
partd==1.4.2
pillow==11.2.1
Pint==0.24.4
platformdirs==4.3.8
ploomber==0.23.3
ploomber-core==0.2.26
ploomber-engine==0.0.33
ploomber-extension==0.1.1
ploomber-scaffold==0.3.1
plotly==6.2.0
pluggy==1.6.0
polars==1.31.0
posthog==5.4.0
pre_commit==4.2.0
prometheus_client==0.22.1
prompt_toolkit==3.0.51
propcache==0.3.2
psutil==7.0.0
pure_eval==0.2.3
py-cpuinfo==9.0.0
pyarrow==20.0.0
pycodestyle==2.14.0
pycparser==2.22
pydantic==2.11.7
pydantic_core==2.33.2
pyflakes==3.4.0
Pygments==2.19.2
pyogrio==0.11.0
pyparsing==3.2.3
pyproj==3.7.1
pypsa==0.35.0
pyshp==2.3.1
pytest==8.2.2
python-calamine==0.3.2
python-dateutil==2.9.0.post0
python-json-logger==3.3.0
pytz==2025.2
pywin32==310
pywinpty==2.0.15
PyYAML==6.0.2
pyzmq==27.0.0
referencing==0.36.2
requests==2.32.4
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
roman-numerals-py==3.1.0
rpds-py==0.25.1
scikit-learn==1.7.0
scikit-learn-extra==0.3.0
scipy==1.16.0
seaborn==0.13.2
Send2Trash==1.8.3
setuptools==80.9.0
shapely==2.0.7
six==1.17.0
sniffio==1.3.1
snowballstemmer==3.0.1
soupsieve==2.7
Sphinx==8.2.3
sphinx-rtd-theme==3.0.2
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
SQLAlchemy==2.0.41
sqlparse==0.5.3
stack-data==0.6.3
tables==3.10.2
tabulate==0.9.0
tenacity==9.1.2
terminado==0.18.1
threadpoolctl==3.6.0
tinycss2==1.4.0
toml==0.10.2
toolz==1.0.0
tornado==6.5.1
tqdm==4.67.1
traitlets==5.14.3
types-python-dateutil==2.9.0.20250516
typing-inspection==0.4.1
typing_extensions==4.14.0
tzdata==2025.2
uri-template==1.3.0
urllib3==2.5.0
validators==0.35.0
virtualenv==20.31.2
wcwidth==0.2.13
webcolors==24.11.1
webencodings==0.5.1
websocket-client==1.8.0
xarray==2025.6.1
xyzservices==2025.4.0
yarl==1.20.1


Discussion:
Thanks for reporting @BuiMCanmet !



Patch:
diff --git a/doc/references/release-notes.rst b/doc/references/release-notes.rst
index 88c7d24fe..858ecb8bc 100644
--- a/doc/references/release-notes.rst
+++ b/doc/references/release-notes.rst
@@ -14,6 +14,9 @@ Upcoming Release
 Bug Fixes
 ---------
 
+* Fixed issue when copying a network with an unsolved model.
+  (https://github.com/PyPSA/PyPSA/pull/5)
+
 * Fixed missing dependency issue for `typing-extensions`
   (https://github.com/PyPSA/PyPSA/pull/1264)
 
diff --git a/pypsa/networks.py b/pypsa/networks.py
index ca6d5bac2..501ada77c 100644
--- a/pypsa/networks.py
+++ b/pypsa/networks.py
@@ -17,6 +17,7 @@
 import pyproj
 import validators
 from deprecation import deprecated
+from linopy import Model
 from pyproj import CRS, Transformer
 from scipy.sparse import csgraph
 
@@ -366,6 +367,7 @@ def equals(self, other: Any, log_mode: str = "silent") -> bool:
             StatisticsAccessor,
             PlotAccessor,
             AbstractStatisticsAccessor,
+            Model,
         ]
         not_equal = False
         if isinstance(other, self.__class__):
@@ -514,8 +516,9 @@ def model(self) -> linopy.Model:
 
         """
         if self._model is None:
-            msg = "The network has not been optimized yet and no model is stored."
-            raise ValueError(msg)
+            logger.warning(
+                "The network has not been optimized yet and no model is stored."
+            )
         return self._model
 
     @model.deleter
@@ -524,7 +527,7 @@ def model(self) -> None:
         self._model = None
 
     @property
-    def objective(self) -> float:
+    def objective(self) -> float | None:
         """Objective value of the solved network.
 
         The property yields the objective value of the solved network. It is set after
@@ -546,8 +549,9 @@ def objective(self) -> float:
 
         """
         if self._objective is None:
-            msg = "The network has not been optimized yet and no objective value is stored."
-            raise ValueError(msg)
+            logger.warning(
+                "The network has not been optimized yet and no objective value is stored."
+            )
         return self._objective
 
     @objective.setter
@@ -562,7 +566,7 @@ def objective(self, new: float) -> None:
         self._objective = new
 
     @property
-    def objective_constant(self) -> float:
+    def objective_constant(self) -> float | None:
         """Objective constant of the network.
 
         The property yields the fixed part of the objective function. It is set after
@@ -583,8 +587,9 @@ def objective_constant(self) -> float:
 
         """
         if self._objective_constant is None:
-            msg = "The network has not been optimized yet and no objective constant is stored."
-            raise ValueError(msg)
+            logger.warning(
+                "The network has not been optimized yet and no objective constant is stored."
+            )
         return self._objective_constant
 
     @objective_constant.setter
@@ -598,6 +603,27 @@ def objective_constant(self, new: float) -> None:
         )
         self._objective_constant = new
 
+    @property
+    def is_solved(self) -> bool:
+        """Check if the network has been solved.
+
+        A solved network has an [objective][pypsa.Network.objective][] value assigned. A
+        [model][pypsa.Network.model][] does not necessarily need to be stored in the
+        network.
+
+        Returns
+        -------
+        bool
+            True if the network has been solved, False otherwise.
+
+        Examples
+        --------
+        >>> n.is_solved
+        True
+
+        """
+        return self._objective is not None
+
     @property
     def crs(self) -> Any:
         """Coordinate reference system of the network's geometries.
@@ -746,9 +772,10 @@ def copy(
         DatetimeIndex(['2015-01-01'], dtype='datetime64[ns]', name='snapshot', freq=None)
 
         """
-        if self._model is not None and self._model.solver_model is not None:
-            msg = "Copying solved networks is not supported yet."
-            raise NotImplementedError(msg)
+        if self.is_solved and hasattr(self.model, "solver_model"):
+            msg = "Copying a solved network with an attached solver model is not supported."
+            msg += " Please delete the model first using `n.model.solver_model = None`."
+            raise ValueError(msg)
 
         # Use copy.deepcopy if no arguments are passed
         args = [snapshots, investment_periods, ignore_standard_types, with_time]
diff --git a/pypsa/optimization/abstract.py b/pypsa/optimization/abstract.py
index 25d83b775..47350ca61 100644
--- a/pypsa/optimization/abstract.py
+++ b/pypsa/optimization/abstract.py
@@ -753,7 +753,7 @@ def optimize_mga(
             }
 
         # check that network has been solved
-        if not hasattr(self._n, "objective"):
+        if not self._n.is_solved:
             msg = "Network needs to be solved with `n.optimize()` before running MGA."
             raise ValueError(msg)
 


Test patch:
diff --git a/test/test_network.py b/test/test_network.py
index a155a89fe..7d9fbb6bf 100644
--- a/test/test_network.py
+++ b/test/test_network.py
@@ -1,6 +1,7 @@
 import copy
 import sys
 
+import linopy
 import numpy as np
 import pandas as pd
 import pytest
@@ -374,6 +375,30 @@ def test_copy_default_behavior(network_all):
     assert n is not network_copy
 
 
+def test_copy_with_model(ac_dc_network):
+    n = ac_dc_network
+    n.optimize.create_model()
+    n_copy = n.copy()
+
+    assert n.equals(n_copy, log_mode="strict")
+    assert isinstance(n.model, linopy.Model)
+    assert isinstance(n_copy.model, linopy.Model)
+
+    n.optimize.solve_model()
+    with pytest.raises(
+        ValueError,
+        match="Copying a solved network with an attached solver model is not supported.",
+    ):
+        n_copy = n.copy()
+
+    n.optimize()
+    with pytest.raises(
+        ValueError,
+        match="Copying a solved network with an attached solver model is not supported.",
+    ):
+        n_copy = n.copy()
+
+
 @pytest.mark.skipif(
     sys.platform == "win32",
     reason="pd.equals fails on windows (https://stackoverflow.com/questions/62128721).",