dsh-eval-guard 0.7.4 → 0.7.6

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.
Files changed (31) hide show
  1. package/ISSUES.md +4 -2
  2. package/README.md +3 -3
  3. package/assets/neural-pendulum/SPEC.md +40 -42
  4. package/assets/neural-pendulum/TASK.md +34 -47
  5. package/assets/neural-pendulum/sim.cpp +97 -45
  6. package/assets/neural-pendulum/sim_agent +0 -0
  7. package/assets/neural-pendulum/sim_eval +0 -0
  8. package/assets/neural-pendulum/workspace.zip +0 -0
  9. package/lib/index.js +27 -10
  10. package/neural-pendulum-eval/README.md +4 -3
  11. package/neural-pendulum-eval/run_eval.py +133 -70
  12. package/neural-pendulum-eval/sim_eval +0 -0
  13. package/package.json +1 -1
  14. package/assets/neural-pendulum/rl-feasibility/README.md +0 -60
  15. package/assets/neural-pendulum/rl-feasibility/RESULTS.md +0 -370
  16. package/assets/neural-pendulum/rl-feasibility/evaluate.py +0 -107
  17. package/assets/neural-pendulum/rl-feasibility/finalize_run.py +0 -168
  18. package/assets/neural-pendulum/rl-feasibility/models.py +0 -124
  19. package/assets/neural-pendulum/rl-feasibility/obs.py +0 -205
  20. package/assets/neural-pendulum/rl-feasibility/plot_selected.py +0 -55
  21. package/assets/neural-pendulum/rl-feasibility/reference_controller.py +0 -112
  22. package/assets/neural-pendulum/rl-feasibility/results/curves_selected.png +0 -0
  23. package/assets/neural-pendulum/rl-feasibility/results/reference_controller.json +0 -417
  24. package/assets/neural-pendulum/rl-feasibility/results/summary.json +0 -1599
  25. package/assets/neural-pendulum/rl-feasibility/results/table.md +0 -39
  26. package/assets/neural-pendulum/rl-feasibility/sim.py +0 -324
  27. package/assets/neural-pendulum/rl-feasibility/summarize.py +0 -175
  28. package/assets/neural-pendulum/rl-feasibility/task.py +0 -342
  29. package/assets/neural-pendulum/rl-feasibility/test_contract.py +0 -188
  30. package/assets/neural-pendulum/rl-feasibility/train_ppo.py +0 -403
  31. package/assets/neural-pendulum/rl-feasibility/train_sac.py +0 -406
@@ -1,124 +0,0 @@
1
- """Networks: PPO actor-critic MLP, SAC squashed-Gaussian actor, twin Q.
2
-
3
- PPO: 2x128 tanh, separate actor and critic, state-independent log_std capped
4
- to [-2.3, -0.25] (the recipe of the sibling rounds).
5
- SAC: 2x256 tanh actor with tanh-Gaussian head and a shared-trunk twin Q (two
6
- output heads). Sharing the critic trunk keeps the CPU update affordable while
7
- preserving the double-Q target.
8
- """
9
- from __future__ import annotations
10
-
11
- import math
12
-
13
- import numpy as np
14
- import torch
15
- import torch.nn as nn
16
-
17
-
18
- class MLP(nn.Module):
19
- def __init__(self, in_dim, hidden=128, depth=2, out_dim=1, out_gain=0.01):
20
- super().__init__()
21
- layers = []
22
- d = in_dim
23
- for _ in range(depth):
24
- lin = nn.Linear(d, hidden)
25
- nn.init.orthogonal_(lin.weight, gain=np.sqrt(2))
26
- nn.init.zeros_(lin.bias)
27
- layers += [lin, nn.Tanh()]
28
- d = hidden
29
- self.body = nn.Sequential(*layers)
30
- self.out = nn.Linear(d, out_dim)
31
- nn.init.orthogonal_(self.out.weight, gain=out_gain)
32
- nn.init.zeros_(self.out.bias)
33
-
34
- def forward(self, x):
35
- return self.out(self.body(x))
36
-
37
-
38
- # ------------------------------- PPO ---------------------------------------
39
- LOG_STD_MIN, LOG_STD_MAX = -2.3, -0.25
40
-
41
-
42
- class ActorCritic(nn.Module):
43
- def __init__(self, obs_dim, act_dim=2, hidden=128, depth=2):
44
- super().__init__()
45
- self.act_dim = act_dim
46
- self.actor = MLP(obs_dim, hidden, depth, act_dim)
47
- self.critic = MLP(obs_dim, hidden, depth, 1, out_gain=1.0)
48
- self.log_std = nn.Parameter(torch.full((act_dim,), -0.7))
49
-
50
- def dist(self, obs):
51
- mean = self.actor(obs)
52
- std = self.effective_log_std().exp().expand_as(mean)
53
- return torch.distributions.Normal(mean, std)
54
-
55
- def effective_log_std(self):
56
- return self.log_std.clamp(LOG_STD_MIN, LOG_STD_MAX)
57
-
58
- def value(self, obs):
59
- return self.critic(obs).squeeze(-1)
60
-
61
- def deterministic_action(self, obs):
62
- return self.actor(obs)
63
-
64
-
65
- # ------------------------------- SAC ---------------------------------------
66
- SAC_LOG_STD_MIN, SAC_LOG_STD_MAX = -5.0, 2.0
67
-
68
-
69
- class SquashedGaussianActor(nn.Module):
70
- def __init__(self, in_dim, act_dim=2, hidden=256, depth=2):
71
- super().__init__()
72
- self.body = nn.Sequential()
73
- d = in_dim
74
- for _ in range(depth):
75
- lin = nn.Linear(d, hidden)
76
- nn.init.orthogonal_(lin.weight, gain=math.sqrt(2))
77
- nn.init.zeros_(lin.bias)
78
- self.body.extend([lin, nn.Tanh()])
79
- d = hidden
80
- self.mean = nn.Linear(hidden, act_dim)
81
- self.log_std = nn.Linear(hidden, act_dim)
82
- nn.init.orthogonal_(self.mean.weight, gain=0.01)
83
- nn.init.zeros_(self.mean.bias)
84
- nn.init.orthogonal_(self.log_std.weight, gain=0.01)
85
- nn.init.zeros_(self.log_std.bias)
86
- self.act_dim = act_dim
87
-
88
- def forward(self, obs):
89
- h = self.body(obs)
90
- return self.mean(h), self.log_std(h).clamp(SAC_LOG_STD_MIN, SAC_LOG_STD_MAX)
91
-
92
- def sample(self, obs, det=False):
93
- if det:
94
- return torch.tanh(self.mean(self.body(obs))), None
95
- mean, log_std = self(obs)
96
- std = log_std.exp()
97
- eps = torch.randn_like(mean)
98
- x = mean + std * eps
99
- a = torch.tanh(x)
100
- logp = (-0.5 * eps ** 2 - log_std - 0.5 * math.log(2 * math.pi)).sum(-1)
101
- logp = logp - torch.log(1 - a ** 2 + 1e-6).sum(-1)
102
- return a, logp
103
-
104
-
105
- class TwinQ(nn.Module):
106
- """Shared trunk, two Q heads; forward returns (B, 2)."""
107
-
108
- def __init__(self, in_dim, act_dim=2, hidden=256, depth=2):
109
- super().__init__()
110
- self.body = nn.Sequential()
111
- d = in_dim + act_dim
112
- for _ in range(depth):
113
- lin = nn.Linear(d, hidden)
114
- nn.init.orthogonal_(lin.weight, gain=math.sqrt(2))
115
- nn.init.zeros_(lin.bias)
116
- self.body.extend([lin, nn.Tanh()])
117
- d = hidden
118
- self.q = nn.Linear(hidden, 2)
119
- nn.init.orthogonal_(self.q.weight, gain=1.0)
120
- nn.init.zeros_(self.q.bias)
121
-
122
- def forward(self, obs, act):
123
- x = torch.cat([obs, act], dim=-1)
124
- return self.q(self.body(x))
@@ -1,205 +0,0 @@
1
- """Policy-side angle observation for the 3-link pendulum (switchable modes).
2
-
3
- The simulator only exposes the three joint angles theta. The observation is
4
- built entirely from that angle stream (no omega, no tip, no energy):
5
-
6
- a_i(t) = wrap_to_pi(theta_i(t) - pi) (upright-relative)
7
- da_i(t) = wrap_to_pi(a_i(t) - a_i(t-1)) / DT, da_i(0) = 0 at reset
8
-
9
- frame(t) = [cos a1, sin a1, cos a2, sin a2, cos a3, sin a3,
10
- da1/5, da2/5, da3/5] (9 dims, diff=True)
11
- frame(t) = [cos a1, sin a1, cos a2, sin a2, cos a3, sin a3]
12
- (6 dims, diff=False)
13
-
14
- obs(t) = H frames, oldest first, flattened
15
-
16
- Modes (--obs):
17
- full H=4, diff=True -> 36 dims (the original observation)
18
- angle H=4, diff=False -> 24 dims (cos/sin only, no manual velocity)
19
- full_h1 H=1, diff=True -> 9 dims (single frame incl. da/5)
20
- angle_h1 H=1, diff=False -> 6 dims (single frame cos/sin only)
21
- full_h2 H=2, diff=True -> 18 dims (extra mode, optional)
22
- angle_h2 H=2, diff=False -> 12 dims (extra mode, optional)
23
-
24
- Even when diff=False the builder still computes da internally, because the
25
- trainer-side reward uses it; the channel is only left out of the policy
26
- observation. theta is used in radians exactly as produced by the simulator
27
- (never divided by pi). The history is initialised by repeating the reset
28
- frame H times.
29
- """
30
- from __future__ import annotations
31
-
32
- import numpy as np
33
-
34
- DT = 0.01
35
- H = 4
36
- VEL_SCALE = 5.0
37
-
38
- # mode -> (history length, whether the manual-difference velocity is observed)
39
- OBS_MODES = {
40
- "full": dict(h=4, diff=True),
41
- "angle": dict(h=4, diff=False),
42
- "full_h1": dict(h=1, diff=True),
43
- "angle_h1": dict(h=1, diff=False),
44
- "full_h2": dict(h=2, diff=True),
45
- "angle_h2": dict(h=2, diff=False),
46
- "angle_h8": dict(h=8, diff=False), # extra: can longer history replace da?
47
- }
48
-
49
-
50
- def frame_dim(diff: bool) -> int:
51
- return 9 if diff else 6
52
-
53
-
54
- def obs_spec(mode: str = "full") -> dict:
55
- if mode not in OBS_MODES:
56
- raise ValueError(f"unknown obs mode {mode!r}; choose from {sorted(OBS_MODES)}")
57
- c = OBS_MODES[mode]
58
- fd = frame_dim(c["diff"])
59
- notes = ("cos/sin(a) + da/5, H=%d, a=wrap(theta-pi), da is the manual "
60
- "finite difference, da(0)=0" % c["h"])
61
- if not c["diff"]:
62
- notes = ("cos/sin(a) only (NO velocity channel), H=%d, "
63
- "a=wrap(theta-pi)" % c["h"])
64
- return dict(mode=mode, enc="sincos", diff=bool(c["diff"]), h=int(c["h"]),
65
- vel_scale=VEL_SCALE, dt=DT, frame_dim=fd,
66
- dim=int(c["h"] * fd), notes=notes)
67
-
68
-
69
- # Backwards-compatible constants for the original full observation.
70
- FRAME_DIM = frame_dim(True)
71
- OBS_DIM = obs_spec("full")["dim"]
72
-
73
-
74
- def wrap_to_pi(a):
75
- return (a + np.pi) % (2.0 * np.pi) - np.pi
76
-
77
-
78
- def frame_np(a, da, diff=True):
79
- a = np.asarray(a, dtype=np.float64)
80
- cs = np.empty(6, dtype=np.float64)
81
- cs[0::2] = np.cos(a)
82
- cs[1::2] = np.sin(a)
83
- if not diff:
84
- return cs
85
- return np.concatenate([cs, np.asarray(da, dtype=np.float64) / VEL_SCALE])
86
-
87
-
88
- class NumpyAngleObs:
89
- """Float64 single-env builder. reset/update take theta only."""
90
-
91
- def __init__(self, h: int = H, vel_scale: float = VEL_SCALE, dt: float = DT,
92
- diff: bool = True):
93
- self.h = int(h)
94
- self.diff = bool(diff)
95
- self.fdim = frame_dim(self.diff)
96
- self.vel_scale = float(vel_scale)
97
- self.dt = float(dt)
98
- self.a_prev = np.zeros(3)
99
- self.da = np.zeros(3)
100
- self.frames = np.zeros((self.h, self.fdim))
101
- self._obs = np.zeros(self.h * self.fdim, dtype=np.float32)
102
-
103
- def _frame(self, a, da):
104
- cs = np.empty(6, dtype=np.float64)
105
- cs[0::2] = np.cos(a)
106
- cs[1::2] = np.sin(a)
107
- if not self.diff:
108
- return cs
109
- return np.concatenate([cs, da / self.vel_scale])
110
-
111
- def _flatten(self):
112
- self._obs = self.frames.reshape(-1).astype(np.float32)
113
- return self._obs
114
-
115
- def reset(self, theta):
116
- a = wrap_to_pi(np.asarray(theta, dtype=np.float64).reshape(3) - np.pi)
117
- self.a_prev = a.copy()
118
- self.da = np.zeros(3)
119
- f0 = self._frame(a, self.da)
120
- self.frames = np.tile(f0, (self.h, 1))
121
- return self._flatten()
122
-
123
- @property
124
- def obs(self):
125
- return self._obs
126
-
127
- def update(self, theta):
128
- a = wrap_to_pi(np.asarray(theta, dtype=np.float64).reshape(3) - np.pi)
129
- self.da = wrap_to_pi(a - self.a_prev) / self.dt
130
- self.a_prev = a.copy()
131
- f = self._frame(a, self.da)
132
- if self.h > 1:
133
- self.frames = np.roll(self.frames, -1, axis=0)
134
- self.frames[-1] = f
135
- return self._flatten()
136
-
137
-
138
- class TorchAngleObs:
139
- """Batched float32 builder. reset/update take theta only."""
140
-
141
- def __init__(self, n: int, h: int = H, vel_scale: float = VEL_SCALE,
142
- dt: float = DT, device="cpu", diff: bool = True):
143
- import torch
144
- self.torch = torch
145
- self.n = int(n)
146
- self.h = int(h)
147
- self.diff = bool(diff)
148
- self.fdim = frame_dim(self.diff)
149
- self.vel_scale = float(vel_scale)
150
- self.dt = float(dt)
151
- self.device = device
152
- self.a_prev = torch.zeros(n, 3, device=device)
153
- self.da = torch.zeros(n, 3, device=device)
154
- self.frames = torch.zeros(n, self.h, self.fdim, device=device)
155
-
156
- def _frame(self, a, da):
157
- t = self.torch
158
- cs = t.stack([t.cos(a), t.sin(a)], dim=2).reshape(a.shape[0], 6)
159
- if not self.diff:
160
- return cs
161
- return t.cat([cs, da / self.vel_scale], dim=1)
162
-
163
- def obs(self):
164
- return self.frames.reshape(self.n, -1)
165
-
166
- def reset(self, theta, idx=None):
167
- t = self.torch
168
- theta = t.as_tensor(theta, dtype=t.float32, device=self.device).reshape(-1, 3)
169
- if idx is None:
170
- a = t.remainder(theta - np.pi + np.pi, 2 * np.pi) - np.pi
171
- self.a_prev = a.clone()
172
- self.da = t.zeros_like(a)
173
- f0 = self._frame(a, self.da)
174
- self.frames = f0.unsqueeze(1).expand(-1, self.h, -1).clone()
175
- return self.obs()
176
- idx = t.as_tensor(idx, dtype=t.long, device=self.device).flatten()
177
- a = t.remainder(theta - np.pi + np.pi, 2 * np.pi) - np.pi
178
- self.a_prev[idx] = a
179
- self.da[idx] = 0.0
180
- f0 = self._frame(a, t.zeros_like(a))
181
- self.frames[idx] = f0.unsqueeze(1).expand(-1, self.h, -1)
182
- return self.obs()
183
-
184
- def update(self, theta):
185
- t = self.torch
186
- theta = t.as_tensor(theta, dtype=t.float32, device=self.device).reshape(-1, 3)
187
- a = t.remainder(theta - np.pi + np.pi, 2 * np.pi) - np.pi
188
- self.da = (t.remainder(a - self.a_prev + np.pi, 2 * np.pi) - np.pi) / self.dt
189
- self.a_prev = a.clone()
190
- f = self._frame(a, self.da)
191
- if self.h > 1:
192
- self.frames = t.cat([self.frames[:, 1:], f.unsqueeze(1)], dim=1)
193
- else:
194
- self.frames[:, -1] = f
195
- return self.obs()
196
-
197
-
198
- def make_numpy_obs(mode: str = "full", **kw):
199
- c = OBS_MODES[mode]
200
- return NumpyAngleObs(h=c["h"], diff=c["diff"], **kw)
201
-
202
-
203
- def make_torch_obs(n: int, mode: str = "full", **kw):
204
- c = OBS_MODES[mode]
205
- return TorchAngleObs(n, h=c["h"], diff=c["diff"], **kw)
@@ -1,55 +0,0 @@
1
- """Plot selected eval t_can curves (PPO left, SAC right)."""
2
- from __future__ import annotations
3
-
4
- import json
5
- from pathlib import Path
6
-
7
- import matplotlib
8
- matplotlib.use("Agg")
9
- import matplotlib.pyplot as plt
10
-
11
- ROOT = Path(__file__).resolve().parent
12
- RES = ROOT / "results"
13
-
14
- PPO = ["ppo_full_base_rand", "ppo_full_base_rand_s1",
15
- "ppo_full_base_rand_s2", "ppo_angle_base_rand", "ppo_angle_h8_rand",
16
- "ppo_full_h1_rand", "ppo_full_h1_mix", "ppo_full_base_fixed"]
17
- SAC = ["sac_full_base_rand", "sac_full_base_fixed", "sac_angle_base_rand"]
18
-
19
-
20
- def load(exp):
21
- p = RES / (exp + ".json")
22
- if not p.exists():
23
- return None
24
- with open(p) as f:
25
- return json.load(f)
26
-
27
-
28
- def main():
29
- fig, axes = plt.subplots(1, 2, figsize=(14, 5.0), sharey=True)
30
- for ax, names, title in [(axes[0], PPO, "PPO"), (axes[1], SAC, "SAC")]:
31
- for exp in names:
32
- r = load(exp)
33
- if not r:
34
- continue
35
- c = r.get("eval_curve") or []
36
- if not c:
37
- continue
38
- x = [v["global_steps"] / 1e6 for v in c]
39
- y = [v["t_can"] for v in c]
40
- ax.plot(x, y, lw=1.3, label=f"{exp} (best {r['best_t_can']:.2f}s)")
41
- ax.axhline(10.0, color="k", ls=":", lw=1)
42
- ax.set_title(f"{title}: fixed-initial eval t_can")
43
- ax.set_xlabel("environment steps (M)")
44
- ax.set_ylabel("t_can (s)")
45
- ax.grid(alpha=0.3)
46
- ax.legend(fontsize=7)
47
- fig.tight_layout()
48
- out = RES / "curves_selected.png"
49
- fig.savefig(out, dpi=130)
50
- plt.close(fig)
51
- print(out)
52
-
53
-
54
- if __name__ == "__main__":
55
- main()
@@ -1,112 +0,0 @@
1
- """Non-RL feasibility check: a pole-placement stabiliser around upright.
2
-
3
- This is NOT a learned policy and NOT part of the RL results. It answers a
4
- single question: is the upright hold physically reachable with the fixed
5
- torque mapping (tau1, -tau3, tau3) and |u| <= 1?
6
-
7
- The controller uses privileged full state (theta and the simulator omega),
8
- so it is an upper bound / existence proof only. It is deliberately simple:
9
- u = clip(-K [wrap(theta-pi); omega], -1, 1)
10
- with K from pole placement on the linearisation at upright.
11
-
12
- Writes results/reference_controller.json.
13
- """
14
- from __future__ import annotations
15
-
16
- import json
17
- from pathlib import Path
18
-
19
- import numpy as np
20
- from scipy.signal import place_poles
21
-
22
- from sim import EPISODE_STEPS, N_ACT, Params, PendulumSim, make_torch_consts
23
- from task import EVAL_OMEGA0, EVAL_THETA0, NumpyTracker, sample_train_init, wrap_to_pi
24
-
25
- ROOT = Path(__file__).resolve().parent
26
-
27
-
28
- def design(p: Params, poles=(-5.0, -6.0, -7.0, -8.0, -9.0, -10.0)):
29
- M = p.A
30
- K = np.diag(p.g * p.gvec)
31
- Minv = np.linalg.inv(M)
32
- B = np.zeros((3, 2))
33
- B[0, 0] = 1.0
34
- B[1, 1] = -1.0
35
- B[2, 1] = 1.0
36
- Be = Minv @ B
37
- A = np.zeros((6, 6))
38
- A[:3, 3:] = np.eye(3)
39
- A[3:, :3] = Minv @ K
40
- B6 = np.vstack([np.zeros((3, 2)), Be])
41
- res = place_poles(A, B6, np.asarray(poles, dtype=float))
42
- return res.gain_matrix
43
-
44
-
45
- def rollout(p, gain, theta0, omega0, episode_steps=EPISODE_STEPS,
46
- alpha=0.5):
47
- sim = PendulumSim(p, episode_steps=episode_steps)
48
- tr = NumpyTracker()
49
- theta = sim.reset(theta0, omega0)
50
- tr.reset()
51
- sat = 0
52
- uf = np.zeros(2)
53
- for _ in range(episode_steps):
54
- x = np.r_[wrap_to_pi(theta - np.pi), sim._omega]
55
- raw = np.clip(-gain @ x, -1.0, 1.0)
56
- # first-order low-pass on the torque command: the unfiltered
57
- # pole-placement controller chatters at 100 Hz and only holds ~1 s.
58
- uf = alpha * raw + (1.0 - alpha) * uf
59
- sat += int(np.abs(uf).max() > 1.0)
60
- theta = sim.step(uf)
61
- tr.update(theta, 0.0)
62
- summ = tr.summary()
63
- return dict(t_can=float(summ["t_can"]),
64
- ever_up=float(summ["ever_up"]),
65
- final_ang_err=float(np.abs(wrap_to_pi(theta - np.pi)).max()),
66
- sat_frac=float(sat / episode_steps),
67
- final_theta=theta.tolist())
68
-
69
-
70
- def main():
71
- p = Params()
72
- gain = design(p)
73
- # This is a LOCAL stabiliser: start near upright (theta = pi + offset).
74
- fixed_theta0 = np.pi + EVAL_THETA0
75
- fixed = rollout(p, gain, fixed_theta0, EVAL_OMEGA0)
76
- rng = np.random.default_rng(0)
77
- th = np.pi + rng.uniform(-0.05, 0.05, size=(32, 3))
78
- om = np.zeros((32, 3))
79
- rows = [rollout(p, gain, th[i], om[i]) for i in range(th.shape[0])]
80
- tc = np.array([r["t_can"] for r in rows])
81
- out = dict(
82
- note=("Non-RL existence check. Pole-placement stabiliser using "
83
- "privileged full state (theta and simulator omega); not a "
84
- "learned policy. Shows whether the upright hold is physically "
85
- "reachable with the fixed torque mapping. The torque command is "
86
- "low-pass filtered (alpha=0.5); the unfiltered controller "
87
- "chatters and only holds ~1 s."),
88
- poles=[-5.0, -6.0, -7.0, -8.0, -9.0, -10.0],
89
- action_filter_alpha=0.5,
90
- gain=gain.tolist(),
91
- fixed_start=dict(theta0=fixed_theta0.tolist(),
92
- offset_from_upright=EVAL_THETA0.tolist(),
93
- omega0=EVAL_OMEGA0.tolist(), **fixed),
94
- random_starts=dict(n=len(rows), success_rate=float((tc >= 10.0).mean()),
95
- mean_t_can=float(tc.mean()),
96
- median_t_can=float(np.median(tc)),
97
- max_t_can=float(tc.max()),
98
- rows=rows),
99
- )
100
- out_path = ROOT / "results" / "reference_controller.json"
101
- with open(out_path, "w") as f:
102
- json.dump(out, f, indent=2)
103
- print(json.dumps(dict(fixed_t_can=fixed["t_can"],
104
- fixed_final_err=fixed["final_ang_err"],
105
- fixed_sat=fixed["sat_frac"],
106
- random_success=out["random_starts"]["success_rate"],
107
- random_mean_t_can=out["random_starts"]["mean_t_can"]),
108
- indent=2))
109
-
110
-
111
- if __name__ == "__main__":
112
- main()