Repository: pypsa/pypsa
Issue: SCLOPF returns TypeError with pyomo=False unless run with pyomo=True first

## Checklist

- [x] I am using the current [`master`](https://github.com/pypsa/pypsa/tree/master) branch or the latest [release](https://github.com/pypsa/pypsa/releases). Please indicate:

## Describe the Bug

Running `sclopf` with `pyomo=False` returns a concatenation-related `TypeError`. However, running `sclopf` with `pyomo=True` on the same network and then running with `pyomo=False` again works. This explains why the unit test (which runs in a loop with True first then False) does not fail. First observed this on my own case then reproduced using the unit test case.

## (MWE and) Error Message
MWE (repurposed from `test_sclopf_scigrid.py`):

```python
In [1]: import pypsa

In [2]: n = pypsa.Network("../../PyPSA/examples/scigrid-de/scigrid-with-load-gen-trafos/")
WARNING:pypsa.io:
Importing PyPSA from older version of PyPSA than current version.
Please read the release notes at https://pypsa.readthedocs.io/en/latest/release_notes.html
carefully to prepare your network for import.
Currently used PyPSA version [0, 20, 0], imported network file PyPSA version [0, 10, 0].

INFO:pypsa.components:Applying weightings to all columns of `snapshot_weightings`
INFO:pypsa.io:Imported network nan has buses, generators, lines, loads, storage_units, transformers

In [3]: for line_name in ["316", "527", "602"]:
   ...:     n.lines.loc[line_name, "s_nom"] = 1200
   ...: branch_outages = n.lines.index[:2]

In [4]: n.sclopf(
   ...:     n.snapshots[0],
   ...:     branch_outages=branch_outages,
   ...:     pyomo=False,  # running with pyomo=False first
   ...:     solver_name="cbc",
   ...: )
INFO:pypsa.linopf:Prepare linear problem
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [4], in <cell line: 1>()
----> 1 n.sclopf(
      2     n.snapshots[0],
      3     branch_outages=branch_outages,
      4     pyomo=False,
      5     solver_name="cbc",
      6 )

File ~/PyPSA/pypsa/contingency.py:464, in network_sclopf(network, snapshots, branch_outages, solver_name, pyomo, skip_pre, extra_functionality, solver_options, keep_files, formulation, ptdf_tolerance)
    460     pyomo_kwargs["ptdf_tolerance"] = ptdf_tolerance
    462 # need to skip preparation otherwise it recalculates the sub-networks
--> 464 network.lopf(
    465     snapshots=snapshots,
    466     solver_name=solver_name,
    467     pyomo=pyomo,
    468     skip_pre=True,
    469     extra_functionality=_extra_functionality,
    470     solver_options=solver_options,
    471     keep_files=keep_files,
    472     formulation=formulation,
    473     **pyomo_kwargs,
    474 )

File ~/PyPSA/pypsa/components.py:752, in Network.lopf(self, snapshots, pyomo, solver_name, solver_options, solver_logfile, formulation, keep_files, extra_functionality, multi_investment_periods, **kwargs)
    750     return network_lopf(self, **args)
    751 else:
--> 752     return network_lopf_lowmem(self, **args)

File ~/PyPSA/pypsa/linopf.py:1450, in network_lopf(n, snapshots, solver_name, solver_logfile, extra_functionality, multi_investment_periods, skip_objective, skip_pre, extra_postprocessing, formulation, keep_references, keep_files, keep_shadowprices, solver_options, warmstart, store_basis, solver_dir)
   1447     n.determine_network_topology()
   1449 logger.info("Prepare linear problem")
-> 1450 fdp, problem_fn = prepare_lopf(
   1451     n, snapshots, keep_files, skip_objective, extra_functionality, solver_dir
   1452 )
   1453 fds, solution_fn = mkstemp(prefix="pypsa-solve", suffix=".sol", dir=solver_dir)
   1455 if warmstart == True:

File ~/PyPSA/pypsa/linopf.py:1107, in prepare_lopf(n, snapshots, keep_files, skip_objective, extra_functionality, solver_dir)
   1105 define_storage_unit_constraints(n, snapshots)
   1106 define_store_constraints(n, snapshots)
-> 1107 define_kirchhoff_constraints(n, snapshots)
   1108 define_nodal_balance_constraints(n, snapshots)
   1109 define_global_constraints(n, snapshots)

File ~/PyPSA/pypsa/linopf.py:508, in define_kirchhoff_constraints(n, sns)
    505     snapshots = sns if period == None else sns[sns.get_loc(period)]
    506     cycle_sum.set_index(snapshots, inplace=True)
--> 508     con = write_constraint(n, cycle_sum, "=", 0)
    509     subconstraints.append(con)
    510 if len(subconstraints) == 0:

File ~/PyPSA/pypsa/linopt.py:270, in write_constraint(n, lhs, sense, rhs, axes, mask)
    268     sense = "=" if sense == "==" else sense
    269 lhs, sense, rhs = _str_array(lhs), _str_array(sense), _str_array(rhs)
--> 270 exprs = "c" + _str_array(cons, True) + ":\\n" + lhs + sense + " " + rhs + "\\n\\n"
    271 if mask is not None:
    272     exprs = np.where(mask, exprs, "")

TypeError: can only concatenate str (not "int") to str

In [5]: n.sclopf(
   ...:     n.snapshots[0],
   ...:     branch_outages=branch_outages,
   ...:     pyomo=True,  # running with pyomo=True works
   ...:     solver_name="cbc",
   ...: )
WARNING:pypsa.components:Solving optimisation problem with pyomo.In PyPSA version 0.21 the default will change to ``n.lopf(pyomo=False)``.Explicitly set ``n.lopf(pyomo=True)`` to retain current behaviour.
INFO:pypsa.opf:Building pyomo model using `kirchhoff` formulation
WARNING:pypsa.contingency:No type given for 1, assuming it is a line
WARNING:pypsa.contingency:No type given for 2, assuming it is a line
INFO:pypsa.opf:Solving model using cbc
# ==========================================================
# = Solver Results                                         =
# ==========================================================
# ----------------------------------------------------------
#   Problem Information
# ----------------------------------------------------------
Problem: 
- Name: unknown
  Lower bound: 339758.4578
  Upper bound: 339758.4578
  Number of objectives: 1
  Number of constraints: 6714
  Number of variables: 2486
  Number of nonzeros: 438
  Sense: minimize
# ----------------------------------------------------------
#   Solver Information
# ----------------------------------------------------------
Solver: 
- Status: ok
  User time: -1.0
  System time: 0.13
  Wallclock time: 0.14
  Termination condition: optimal
  Termination message: Model was solved to optimality (subject to tolerances), and an optimal solution is available.
  Statistics: 
    Branch and bound: 
      Number of bounded subproblems: None
      Number of created subproblems: None
    Black box: 
      Number of iterations: 760
  Error rc: 0
  Time: 0.15059161186218262
# ----------------------------------------------------------
#   Solution Information
# ----------------------------------------------------------
Solution: 
- number of solutions: 0
  number of solutions displayed: 0
INFO:pypsa.opf:Optimization successful

In [6]: n.sclopf(
   ...:     n.snapshots[0],
   ...:     branch_outages=branch_outages,
   ...:     pyomo=False,  # after successfully running with pyomo=True, running again with False (same as In[4]) works fine.
   ...:     solver_name="cbc",
   ...: )
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 4.23s
INFO:pypsa.linopf:Solve linear problem using Cbc solver
INFO:pypsa.linopf:Optimization successful. Objective value: 3.40e+05
```


Changed files:
test/conftest.py, pypsa/contingency.py, test/test_sclopf_scigrid.py

diff --git a/pypsa/contingency.py b/pypsa/contingency.py
index 41e550bf..f09f683b 100644
--- a/pypsa/contingency.py
+++ b/pypsa/contingency.py
@@ -433,6 +433,7 @@ def network_sclopf(
     """
 
     if not skip_pre:
+        network.calculate_dependent_values()
         network.determine_network_topology()
 
     snapshots = _as_snapshots(network, snapshots)
diff --git a/test/conftest.py b/test/conftest.py
index 50527ddb..bba54fab 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -15,7 +15,7 @@ import pytest
 import pypsa
 
 
-@pytest.fixture(scope="module")
+@pytest.fixture(scope="function")
 def scipy_network():
     csv_folder = os.path.join(
         os.path.dirname(__file__),
diff --git a/test/test_sclopf_scigrid.py b/test/test_sclopf_scigrid.py
index ae3243f0..a6ff7e0d 100644
--- a/test/test_sclopf_scigrid.py
+++ b/test/test_sclopf_scigrid.py
@@ -1,13 +1,15 @@
 # -*- coding: utf-8 -*-
 
 import numpy as np
+import pytest
 from numpy.testing import assert_almost_equal as equal
 from numpy.testing import assert_array_almost_equal as arr_equal
 
 solver_name = "glpk"
 
 
-def test_sclopf(scipy_network):
+@pytest.mark.parametrize("pyomo", [True, False])
+def test_sclopf_pyomo(scipy_network, pyomo):
     n = scipy_network
 
     # There are some infeasibilities without line extensions
@@ -17,34 +19,27 @@ def test_sclopf(scipy_network):
     # choose the contingencies
     branch_outages = n.lines.index[:2]
 
-    objectives = []
-    for pyomo in [True, False]:
+    n.sclopf(
+        n.snapshots[0],
+        branch_outages=branch_outages,
+        pyomo=pyomo,
+        solver_name=solver_name,
+    )
 
-        n.sclopf(
-            n.snapshots[0],
-            branch_outages=branch_outages,
-            pyomo=pyomo,
-            solver_name=solver_name,
-        )
+    # For the PF, set the P to the optimised P
+    n.generators_t.p_set = n.generators_t.p.copy()
+    n.storage_units_t.p_set = n.storage_units_t.p.copy()
 
-        # For the PF, set the P to the optimised P
-        n.generators_t.p_set = n.generators_t.p.copy()
-        n.storage_units_t.p_set = n.storage_units_t.p.copy()
+    # Check no lines are overloaded with the linear contingency analysis
 
-        # Check no lines are overloaded with the linear contingency analysis
+    p0_test = n.lpf_contingency(n.snapshots[0], branch_outages=branch_outages)
 
-        p0_test = n.lpf_contingency(n.snapshots[0], branch_outages=branch_outages)
+    # check loading as per unit of s_nom in each contingency
 
-        # check loading as per unit of s_nom in each contingency
+    max_loading = (
+        abs(p0_test.divide(n.passive_branches().s_nom, axis=0)).describe().loc["max"]
+    )
 
-        max_loading = (
-            abs(p0_test.divide(n.passive_branches().s_nom, axis=0))
-            .describe()
-            .loc["max"]
-        )
+    arr_equal(max_loading, np.ones((len(max_loading))), decimal=4)
 
-        arr_equal(max_loading, np.ones((len(max_loading))), decimal=4)
-
-        objectives.append(n.objective)
-
-    equal(*objectives, decimal=1)
+    equal(n.objective, 339758.4578, decimal=1)