pysfbox 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pysfbox/__init__.py +33 -0
- pysfbox/__main__.py +24 -0
- pysfbox/inputreader.py +91 -0
- pysfbox/lattice.py +217 -0
- pysfbox/latticend.py +330 -0
- pysfbox/model.py +646 -0
- pysfbox/output.py +98 -0
- pysfbox/reactions.py +217 -0
- pysfbox/runner.py +566 -0
- pysfbox/sfnewton.py +485 -0
- pysfbox/system.py +1457 -0
- pysfbox-1.0.0.dist-info/METADATA +256 -0
- pysfbox-1.0.0.dist-info/RECORD +18 -0
- pysfbox-1.0.0.dist-info/WHEEL +5 -0
- pysfbox-1.0.0.dist-info/entry_points.txt +2 -0
- pysfbox-1.0.0.dist-info/licenses/LICENSE +675 -0
- pysfbox-1.0.0.dist-info/licenses/NOTICE +33 -0
- pysfbox-1.0.0.dist-info/top_level.txt +1 -0
pysfbox/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PySFBox -- the Scheutjens-Fleer self-consistent-field (SF-SCF) lattice theory
|
|
3
|
+
for polymers and surfactants at interfaces, in pure Python/NumPy. It reads and
|
|
4
|
+
writes the input/output file format of the classic sfbox and Namics programs.
|
|
5
|
+
|
|
6
|
+
Scope (v1): one gradient direction (planar / cylindrical / spherical,
|
|
7
|
+
including FJC_choices lattice refinement) plus the Namics two- and
|
|
8
|
+
three-gradient lattices (2D flat, 2D cylindrical, 3D flat); linear
|
|
9
|
+
(multi-block) and branched chains, monomeric solvents, frozen surfaces,
|
|
10
|
+
pinned (grafted) segments, Flory-Huggins chi interactions; charged systems
|
|
11
|
+
(fixed charges, electrodes, and weak/multistate charges via
|
|
12
|
+
state/reaction); var scans and search/super-iteration. Unknown keywords are
|
|
13
|
+
reported and ignored; unknown output properties print "NiN", exactly like
|
|
14
|
+
Namics; unsupported features raise a clear NotImplementedError.
|
|
15
|
+
|
|
16
|
+
Every supported feature is validated against the compiled Namics at or near
|
|
17
|
+
machine precision (regression suite in tests/); the pure-NumPy source doubles
|
|
18
|
+
as a readable reference implementation of the SF-SCF machinery.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
python -m pysfbox path/to/input.in
|
|
22
|
+
|
|
23
|
+
The architecture mirrors Namics (Lattice / Segment / Molecule / System /
|
|
24
|
+
Solver / Output) so that extensions slot in where they live in the C++
|
|
25
|
+
original.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from .system import System
|
|
29
|
+
from .inputreader import read_input
|
|
30
|
+
from .runner import run_file
|
|
31
|
+
|
|
32
|
+
__version__ = "1.0.0"
|
|
33
|
+
__all__ = ["System", "read_input", "run_file", "__version__"]
|
pysfbox/__main__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Command-line entry point:
|
|
2
|
+
python -m pysfbox input.in [input2.in ...] run each file (one process)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from .runner import run_file
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(argv=None):
|
|
11
|
+
args = (argv or sys.argv)[1:]
|
|
12
|
+
if not args or args[0] in ("-h", "--help"):
|
|
13
|
+
print(__doc__)
|
|
14
|
+
print("Runs Scheutjens-Fleer SCF on Namics-format input files;")
|
|
15
|
+
print("writes .kal/.pro output files next to each input file.")
|
|
16
|
+
return 1
|
|
17
|
+
for path in args:
|
|
18
|
+
print(f"== {path}")
|
|
19
|
+
run_file(path)
|
|
20
|
+
return 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
sys.exit(main())
|
pysfbox/inputreader.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Namics input-file parser.
|
|
2
|
+
|
|
3
|
+
Namics input files consist of lines
|
|
4
|
+
|
|
5
|
+
keyword : name : parameter : value
|
|
6
|
+
|
|
7
|
+
with `//` comments, blank lines, and the word `start` on its own line closing
|
|
8
|
+
a calculation. Settings accumulate across `start` blocks (later blocks add to
|
|
9
|
+
or override earlier ones), exactly as in Namics. `alias : X : value : v`
|
|
10
|
+
defines `#X#` substitutions inside composition strings, and a `var` block
|
|
11
|
+
defines a parameter scan within one calculation.
|
|
12
|
+
|
|
13
|
+
Because output-specification lines may repeat a parameter with different
|
|
14
|
+
values (e.g. `kal : mon : G : 1st_M_phi_z` and `kal : mon : G : 2nd_M_phi_z`),
|
|
15
|
+
every parameter stores a LIST of values; use `last(...)` for normal settings
|
|
16
|
+
(override semantics) and the full list for output specs.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from collections import OrderedDict
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_line(line):
|
|
23
|
+
line = line.split("//")[0].strip()
|
|
24
|
+
if not line:
|
|
25
|
+
return None
|
|
26
|
+
if line.lower() == "start":
|
|
27
|
+
return "START"
|
|
28
|
+
parts = [p.strip() for p in line.split(":")]
|
|
29
|
+
if len(parts) < 4:
|
|
30
|
+
raise ValueError(f"cannot parse input line: '{line}'")
|
|
31
|
+
key, name, param = parts[0], parts[1], parts[2]
|
|
32
|
+
value = ":".join(parts[3:]).strip()
|
|
33
|
+
return key, name, param, value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read_input(path):
|
|
37
|
+
"""Parse a Namics .in file -> list of calculations.
|
|
38
|
+
|
|
39
|
+
Each calculation is settings[(key, name)][param] = [values...], with
|
|
40
|
+
settings accumulated over consecutive `start` blocks.
|
|
41
|
+
"""
|
|
42
|
+
with open(path) as f:
|
|
43
|
+
lines = f.readlines()
|
|
44
|
+
|
|
45
|
+
calculations = []
|
|
46
|
+
settings = OrderedDict()
|
|
47
|
+
block_has_content = False
|
|
48
|
+
for ln in lines:
|
|
49
|
+
parsed = _parse_line(ln)
|
|
50
|
+
if parsed is None:
|
|
51
|
+
continue
|
|
52
|
+
if parsed == "START":
|
|
53
|
+
calculations.append(_copy(settings))
|
|
54
|
+
block_has_content = False
|
|
55
|
+
continue
|
|
56
|
+
key, name, param, value = parsed
|
|
57
|
+
params = settings.setdefault((key, name), OrderedDict())
|
|
58
|
+
params.setdefault(param, []).append(value)
|
|
59
|
+
block_has_content = True
|
|
60
|
+
if block_has_content or not calculations:
|
|
61
|
+
calculations.append(_copy(settings))
|
|
62
|
+
return calculations
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _copy(settings):
|
|
66
|
+
return OrderedDict((k, OrderedDict((p, list(v)) for p, v in d.items()))
|
|
67
|
+
for k, d in settings.items())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def last(params, key, default=None):
|
|
71
|
+
"""Override semantics: the last value given for a parameter."""
|
|
72
|
+
v = params.get(key)
|
|
73
|
+
return v[-1] if v else default
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_blocks(settings, key):
|
|
77
|
+
"""All (name, params) blocks for a keyword, in input order."""
|
|
78
|
+
return [(name, params) for (k, name), params in settings.items()
|
|
79
|
+
if k == key]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def set_value(settings, key, name, param, value):
|
|
83
|
+
settings.setdefault((key, name), OrderedDict())[param] = [str(value)]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def substitute_aliases(text, settings):
|
|
87
|
+
"""Replace #X# by the value of `alias : X : value`."""
|
|
88
|
+
for (key, name), params in settings.items():
|
|
89
|
+
if key == "alias" and "value" in params:
|
|
90
|
+
text = text.replace(f"#{name}#", str(params["value"][-1]))
|
|
91
|
+
return text
|
pysfbox/lattice.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""One-gradient lattice: planar, cylindrical, or spherical.
|
|
2
|
+
|
|
3
|
+
Interior sites z = fjc .. MX+fjc-1 with `fjc` ghost layers on each side for the
|
|
4
|
+
boundary conditions, exactly as in Namics LGrad1.cpp. With fjc = 1 (the default)
|
|
5
|
+
this is the familiar z = 1..MX plus ghosts 0 and MX+1, first-order Markov chains,
|
|
6
|
+
and the three-point step weights lambda_1 (toward z-1), lambda0, lambda1 (toward
|
|
7
|
+
z+1).
|
|
8
|
+
|
|
9
|
+
`FJC_choices > 3` refines the lattice: fjc = (FJC_choices-1)/2 sub-layers per
|
|
10
|
+
bond length, so the internal MX = fjc * n_layers and the propagator becomes a
|
|
11
|
+
(2*fjc+1)-point stencil `LAMBDA[c, z]` (c = 0..FJC-1, neighbour offset c-fjc)
|
|
12
|
+
with curvature-weighted, position-dependent coefficients (Namics
|
|
13
|
+
ComputeLambdas). fjc = 1 keeps the original three-point code bit-for-bit.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
LAMBDA = {"simple_cubic": 1.0 / 6.0, "hexagonal": 0.25}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Lattice1D:
|
|
22
|
+
def __init__(self, n_layers, geometry="planar",
|
|
23
|
+
lattice_type="simple_cubic", lowerbound="mirror",
|
|
24
|
+
upperbound="mirror", offset_first_layer=0.0, fjc=1):
|
|
25
|
+
if geometry == "flat": # Namics accepts flat as planar synonym
|
|
26
|
+
geometry = "planar"
|
|
27
|
+
self.geometry = geometry
|
|
28
|
+
self.lattice_type = lattice_type
|
|
29
|
+
self.lowerbound, self.upperbound = lowerbound, upperbound
|
|
30
|
+
# chain-stiffness defaults (Namics reads Markov/k_stiff at both lat
|
|
31
|
+
# and mol level; System fills these from the lat block and each
|
|
32
|
+
# Molecule may override with its own mol : X : Markov / k_stiff).
|
|
33
|
+
self.markov = 1
|
|
34
|
+
self.k_stiff = 0.0
|
|
35
|
+
self.fjc = int(fjc)
|
|
36
|
+
self.FJC = 2 * self.fjc + 1
|
|
37
|
+
self.offset = float(offset_first_layer)
|
|
38
|
+
self.MX = self.fjc * int(n_layers) # internal (refined) sites
|
|
39
|
+
self.M = self.MX + 2 * self.fjc # incl. fjc ghosts each side
|
|
40
|
+
self.iv = slice(self.fjc, self.M - self.fjc) # interior slice
|
|
41
|
+
self.gradients = 1
|
|
42
|
+
interior = np.zeros(self.M, dtype=bool) # flat interior mask
|
|
43
|
+
interior[self.iv] = True # (uniform ND-style API)
|
|
44
|
+
self.interior = interior
|
|
45
|
+
lam = LAMBDA[lattice_type]
|
|
46
|
+
self.lam = lam
|
|
47
|
+
# physical coordinate of the interior sites (Namics .pro: (k+0.5)/fjc);
|
|
48
|
+
# for fjc=1 this is k+0.5 = z-0.5, matching the original output/moments
|
|
49
|
+
self.z_phys = (np.arange(self.MX) + 0.5) / self.fjc
|
|
50
|
+
|
|
51
|
+
if self.fjc == 1:
|
|
52
|
+
self._setup_fjc1(geometry, lam, offset_first_layer)
|
|
53
|
+
else:
|
|
54
|
+
self._setup_fjc(geometry)
|
|
55
|
+
|
|
56
|
+
# ---- fjc = 1: original three-point lattice (unchanged) ------------------
|
|
57
|
+
def _setup_fjc1(self, geometry, lam, offset_first_layer):
|
|
58
|
+
z = np.arange(self.M, dtype=float) # ghost, 1..MX, ghost
|
|
59
|
+
r = offset_first_layer + z
|
|
60
|
+
if geometry == "planar":
|
|
61
|
+
self.L = np.ones(self.M)
|
|
62
|
+
self.l1 = np.full(self.M, lam) # weight toward z+1
|
|
63
|
+
self.l_1 = np.full(self.M, lam) # weight toward z-1
|
|
64
|
+
elif geometry == "cylindrical":
|
|
65
|
+
self.L = np.pi * (r**2 - (r - 1) ** 2)
|
|
66
|
+
self.l1 = 2 * np.pi * r * lam / self.L
|
|
67
|
+
self.l_1 = 2 * np.pi * (r - 1) * lam / self.L
|
|
68
|
+
elif geometry == "spherical":
|
|
69
|
+
self.L = 4.0 / 3.0 * np.pi * (r**3 - (r - 1) ** 3)
|
|
70
|
+
self.l1 = 4 * np.pi * r**2 * lam / self.L
|
|
71
|
+
self.l_1 = 4 * np.pi * (r - 1) ** 2 * lam / self.L
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError(f"unknown geometry '{geometry}'")
|
|
74
|
+
self.l0 = 1.0 - self.l1 - self.l_1
|
|
75
|
+
self.volume = (self.L[self.iv].sum() if geometry != "planar"
|
|
76
|
+
else float(self.MX))
|
|
77
|
+
|
|
78
|
+
# ---- fjc > 1: refined (2*fjc+1)-point lattice (Namics ComputeLambdas) ---
|
|
79
|
+
def _setup_fjc(self, geometry):
|
|
80
|
+
if geometry not in ("spherical", "cylindrical"):
|
|
81
|
+
raise NotImplementedError(
|
|
82
|
+
f"FJC_choices > 1 with geometry '{geometry}' is not supported "
|
|
83
|
+
"yet (not yet ported to PySFBox); PySFBox has spherical/cylindrical")
|
|
84
|
+
fjc, FJC, MX, M = self.fjc, self.FJC, self.MX, self.M
|
|
85
|
+
off = self.offset
|
|
86
|
+
L = np.zeros(M)
|
|
87
|
+
LAM = np.zeros((FJC, M)) # LAM[c, i], neighbour offset c-fjc
|
|
88
|
+
sph = geometry == "spherical"
|
|
89
|
+
if sph: # sphere: C_ext=1/2 C_mid, area=4r^2
|
|
90
|
+
area = lambda rr: 4.0 * rr * rr
|
|
91
|
+
c_ext, c_mid = 0.5 / (FJC - 1), 1.0 / (FJC - 1)
|
|
92
|
+
else: # cylinder: middle area doubled, area=r
|
|
93
|
+
area = lambda rr: rr
|
|
94
|
+
c_ext, c_mid = 1.0 / (FJC - 1), 2.0 / (FJC - 1)
|
|
95
|
+
for i in range(fjc, M - fjc):
|
|
96
|
+
r = off + (i - fjc + 1.0) / fjc
|
|
97
|
+
rlow, rhigh = r - 0.5, r + 0.5
|
|
98
|
+
if sph:
|
|
99
|
+
L[i] = np.pi * 4.0 / 3.0 * (rhigh**3 - rlow**3) / fjc
|
|
100
|
+
VL = 4.0 / 3.0 * (rhigh**3 - rlow**3)
|
|
101
|
+
else:
|
|
102
|
+
L[i] = np.pi * (2.0 * r) / fjc
|
|
103
|
+
VL = 2.0 * r
|
|
104
|
+
edge = MX / fjc
|
|
105
|
+
# outermost channels (bond endpoints)
|
|
106
|
+
if 2 * rlow - r > 0:
|
|
107
|
+
LAM[0, i] += c_ext * area(rlow) / VL
|
|
108
|
+
if 2 * rhigh - r < edge:
|
|
109
|
+
LAM[FJC - 1, i] += c_ext * area(rhigh) / VL
|
|
110
|
+
else: # reflect off the outer mirror
|
|
111
|
+
self._reflect(LAM, FJC - 1, i, r, rhigh, edge, fjc, c_ext, VL, area)
|
|
112
|
+
# inner channels
|
|
113
|
+
for j in range(1, fjc):
|
|
114
|
+
rlow += 0.5 / fjc
|
|
115
|
+
rhigh -= 0.5 / fjc
|
|
116
|
+
if 2 * rlow - r > 0:
|
|
117
|
+
LAM[j, i] += c_mid * area(rlow) / VL
|
|
118
|
+
if 2 * rhigh - r < off + edge:
|
|
119
|
+
LAM[FJC - 1 - j, i] += c_mid * area(rhigh) / VL
|
|
120
|
+
else:
|
|
121
|
+
self._reflect(LAM, FJC - 1 - j, i, r, rhigh, edge, fjc,
|
|
122
|
+
c_mid, VL, area)
|
|
123
|
+
LAM[fjc, i] += 1.0 - LAM[:, i].sum() # centre closes the row
|
|
124
|
+
self.L, self.LAM = L, LAM
|
|
125
|
+
# geometric volume in closed form (Namics LGrad1.cpp:184-185); this
|
|
126
|
+
# equals sum(L[iv]) at fjc=1 but the interior L-sum over-counts the
|
|
127
|
+
# true volume for fjc>1 (boundary half-cells). Found 5 Jul 2026.
|
|
128
|
+
off = self.offset
|
|
129
|
+
if geometry == "spherical":
|
|
130
|
+
self.volume = 4.0 / 3.0 * np.pi * ((MX + off) ** 3 - off ** 3) \
|
|
131
|
+
/ fjc ** 3
|
|
132
|
+
else: # cylindrical
|
|
133
|
+
self.volume = np.pi * ((MX + off) ** 2 - off ** 2) / fjc ** 2
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _reflect(LAM, c, i, r, rhigh, edge, fjc, coef, VL, area):
|
|
137
|
+
"""Fold a bond that reaches past the outer mirror back into channel c
|
|
138
|
+
(Namics' else-branch: reflected radius rhigh - k/fjc)."""
|
|
139
|
+
d = 2 * rhigh - r - edge
|
|
140
|
+
if -0.001 < d < 0.001:
|
|
141
|
+
LAM[c, i] += coef * area(rhigh) / VL
|
|
142
|
+
for k in range(1, fjc + 1):
|
|
143
|
+
if 0.99 * k / fjc < d < 1.01 * k / fjc:
|
|
144
|
+
LAM[c, i] += coef * area(rhigh - k / fjc) / VL
|
|
145
|
+
|
|
146
|
+
# -- boundary handling -------------------------------------------------
|
|
147
|
+
def set_bounds(self, f, lower_value=None, upper_value=None):
|
|
148
|
+
"""Fill the fjc ghost layers on each side. mirror: reflect the interior;
|
|
149
|
+
surface: 0 (impenetrable solid). Explicit values override the whole
|
|
150
|
+
ghost block (used for frozen surface densities)."""
|
|
151
|
+
f = f.copy()
|
|
152
|
+
fjc, MX = self.fjc, self.MX
|
|
153
|
+
for k in range(fjc):
|
|
154
|
+
lo, hi = k, MX + fjc + k # ghost indices (lower, upper)
|
|
155
|
+
if lower_value is not None:
|
|
156
|
+
f[lo] = lower_value
|
|
157
|
+
elif self.lowerbound == "surface":
|
|
158
|
+
f[lo] = 0.0
|
|
159
|
+
else: # mirror: reflect about the edge
|
|
160
|
+
f[lo] = f[2 * fjc - k - 1]
|
|
161
|
+
if upper_value is not None:
|
|
162
|
+
f[hi] = upper_value
|
|
163
|
+
elif self.upperbound == "surface":
|
|
164
|
+
f[hi] = 0.0
|
|
165
|
+
else:
|
|
166
|
+
f[hi] = f[MX + fjc - k - 1]
|
|
167
|
+
return f
|
|
168
|
+
|
|
169
|
+
def set_mirror_bounds(self, f):
|
|
170
|
+
"""Fill ghost layers by MIRRORING regardless of the boundary type
|
|
171
|
+
(Namics set_M_bounds) — used for the electrostatic potential, whose
|
|
172
|
+
wall condition is zero-field, not zero-value."""
|
|
173
|
+
f = f.copy()
|
|
174
|
+
fjc, MX = self.fjc, self.MX
|
|
175
|
+
for k in range(fjc):
|
|
176
|
+
f[k] = f[2 * fjc - k - 1]
|
|
177
|
+
f[MX + fjc + k] = f[MX + fjc - k - 1]
|
|
178
|
+
return f
|
|
179
|
+
|
|
180
|
+
def site_average(self, f_with_bounds):
|
|
181
|
+
"""<f>(z): three-point (fjc=1) or (2*fjc+1)-point (fjc>1) weighted
|
|
182
|
+
average over neighbours, interior only."""
|
|
183
|
+
out = np.zeros(self.M)
|
|
184
|
+
if self.fjc == 1:
|
|
185
|
+
out[1:-1] = (self.l_1[1:-1] * f_with_bounds[:-2]
|
|
186
|
+
+ self.l0[1:-1] * f_with_bounds[1:-1]
|
|
187
|
+
+ self.l1[1:-1] * f_with_bounds[2:])
|
|
188
|
+
return out
|
|
189
|
+
f, fjc, M = f_with_bounds, self.fjc, self.M
|
|
190
|
+
for c in range(self.FJC):
|
|
191
|
+
d = c - fjc # neighbour offset
|
|
192
|
+
if d == 0:
|
|
193
|
+
out += self.LAM[c] * f
|
|
194
|
+
elif d > 0:
|
|
195
|
+
out[:-d] += self.LAM[c][:-d] * f[d:]
|
|
196
|
+
else:
|
|
197
|
+
out[-d:] += self.LAM[c][-d:] * f[:d]
|
|
198
|
+
return out
|
|
199
|
+
|
|
200
|
+
def propagate(self, gs, G1, lower_value=0.0, upper_value=None):
|
|
201
|
+
"""One chain-propagation step: G1 * <gs>. Propagators see solids as
|
|
202
|
+
0 (default lower ghost 0 when lowerbound is a surface; mirror
|
|
203
|
+
otherwise handled by set_bounds default)."""
|
|
204
|
+
lv = lower_value if self.lowerbound == "surface" else None
|
|
205
|
+
gb = self.set_bounds(gs, lower_value=lv, upper_value=upper_value)
|
|
206
|
+
return G1 * self.site_average(gb)
|
|
207
|
+
|
|
208
|
+
# -- observables --------------------------------------------------------
|
|
209
|
+
def weighted_sum(self, X):
|
|
210
|
+
return float(np.dot(X[self.iv], self.L[self.iv]))
|
|
211
|
+
|
|
212
|
+
def moment(self, X, Xb, n):
|
|
213
|
+
# Namics PutM divides the summed moment by fjc so refined-lattice
|
|
214
|
+
# (fjc>1) moments come out in physical-layer units (factor 1 at
|
|
215
|
+
# fjc=1). Found in the 5 Jul 2026 physics review.
|
|
216
|
+
return float(np.dot(self.z_phys**n, (X[self.iv] - Xb)
|
|
217
|
+
* self.L[self.iv])) / self.fjc
|