PyLTSpice 5.3.2__tar.gz → 5.4.2__tar.gz
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.
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PKG-INFO +30 -7
- pyltspice-5.4.2/PyLTSpice/LTSpiceBatch.py +9 -0
- pyltspice-5.4.2/PyLTSpice/LTSpice_RawRead.py +10 -0
- pyltspice-5.4.2/PyLTSpice/LTSpice_RawWrite.py +10 -0
- pyltspice-5.4.2/PyLTSpice/client_server/sim_client.py +208 -0
- pyltspice-5.4.2/PyLTSpice/client_server/sim_server.py +130 -0
- pyltspice-5.4.2/PyLTSpice/client_server/srv_sim_runner.py +123 -0
- pyltspice-5.4.2/PyLTSpice/raw/raw_convert.py +141 -0
- pyltspice-5.4.2/PyLTSpice/sim/__init__.py +0 -0
- pyltspice-5.4.2/PyLTSpice/sim/ngspice_simulator.py +106 -0
- pyltspice-5.4.2/PyLTSpice/sim/run_task.py +174 -0
- pyltspice-5.4.2/PyLTSpice/sim/sim_analysis.py +105 -0
- pyltspice-5.4.2/PyLTSpice/sim/simulator.py +123 -0
- pyltspice-5.4.2/PyLTSpice/sim/spice_editor.py +1067 -0
- pyltspice-5.4.2/PyLTSpice/sim/xyce_simulator.py +135 -0
- pyltspice-5.4.2/PyLTSpice/sim_batch.py +488 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice.egg-info/PKG-INFO +30 -7
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice.egg-info/SOURCES.txt +32 -0
- pyltspice-5.4.2/PyLTSpice.egg-info/requires.txt +1 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice.egg-info/top_level.txt +1 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/README.md +26 -3
- pyltspice-5.4.2/examples/issue126/testcase/test.py +6 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/pyproject.toml +3 -3
- pyltspice-5.4.2/tests/batch_test.py +50 -0
- pyltspice-5.4.2/tests/batch_test2.py +31 -0
- pyltspice-5.4.2/tests/batch_test3.py +70 -0
- pyltspice-5.4.2/tests/batch_test4.py +65 -0
- pyltspice-5.4.2/tests/batch_test_with_abort.py +48 -0
- pyltspice-5.4.2/tests/fra_example.py +58 -0
- pyltspice-5.4.2/tests/ngspice_batch.py +30 -0
- pyltspice-5.4.2/tests/raw_plotting.py +109 -0
- pyltspice-5.4.2/tests/raw_write_tests.py +108 -0
- pyltspice-5.4.2/tests/raw_write_tools.py +111 -0
- pyltspice-5.4.2/tests/rc_example.py +9 -0
- pyltspice-5.4.2/tests/sim_server_example.py +10 -0
- pyltspice-5.4.2/tests/sketch.py +54 -0
- pyltspice-5.4.2/tests/test_ltsteps.py +20 -0
- pyltspice-5.4.2/tests/unittest/sweep_iterators/sweep_iterators_unittest.py +147 -0
- pyltspice-5.4.2/tests/unittest/test_pyltspice.py +497 -0
- pyltspice-5.3.2/PyLTSpice.egg-info/requires.txt +0 -1
- {pyltspice-5.3.2 → pyltspice-5.4.2}/LICENSE +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/Histogram.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/LTSteps.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/__init__.py +0 -0
- {pyltspice-5.3.2/PyLTSpice/editor → pyltspice-5.4.2/PyLTSpice/client_server}/__init__.py +0 -0
- {pyltspice-5.3.2/PyLTSpice/log → pyltspice-5.4.2/PyLTSpice/editor}/__init__.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/editor/asc_editor.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/editor/spice_editor.py +0 -0
- {pyltspice-5.3.2/PyLTSpice/raw → pyltspice-5.4.2/PyLTSpice/log}/__init__.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/log/logfile_data.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/log/ltsteps.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/log/semi_dev_op_reader.py +0 -0
- {pyltspice-5.3.2/PyLTSpice/sim → pyltspice-5.4.2/PyLTSpice/raw}/__init__.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/raw/raw_classes.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/raw/raw_read.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/raw/raw_write.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/rawplot.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/run_server.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/ltspice_simulator.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/process_callback.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/sim_batch.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/sim_runner.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/sim_stepping.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/tookit/montecarlo.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/tookit/worst_case.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/utils/detect_encoding.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/utils/sweep_iterators.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice.egg-info/dependency_links.txt +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/ltsteps_example.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/raw_plotting.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/raw_read_example.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/raw_write_example.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/run_montecarlo.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/run_worst_case.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/sim_runner_asc_example.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/sim_runner_callback_example.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/sim_runner_callback_process_example.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/sim_runner_example.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/sim_stepper_example.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/spice_editor_example.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/sub_circuit_asc_edits.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/setup.cfg +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/unittests/sweep_iterators_unittest.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/unittests/test_asc_editor.py +0 -0
- {pyltspice-5.3.2 → pyltspice-5.4.2}/unittests/test_pyltspice.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: PyLTSpice
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.4.2
|
|
4
4
|
Summary: A set of tools to Automate LTSpice simulations
|
|
5
5
|
Author-email: Nuno Brum <me@nunobrum.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -685,10 +685,10 @@ Project-URL: Author, https://www.nunobrum.com/
|
|
|
685
685
|
Classifier: Programming Language :: Python :: 3
|
|
686
686
|
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
687
687
|
Classifier: Operating System :: OS Independent
|
|
688
|
-
Requires-Python: >=3.
|
|
688
|
+
Requires-Python: >=3.9
|
|
689
689
|
Description-Content-Type: text/markdown
|
|
690
690
|
License-File: LICENSE
|
|
691
|
-
Requires-Dist: spicelib>=1.
|
|
691
|
+
Requires-Dist: spicelib>=1.4.1
|
|
692
692
|
|
|
693
693
|
# README #
|
|
694
694
|
|
|
@@ -1145,11 +1145,34 @@ _Make sure to initialize the root logger before importing the library to be able
|
|
|
1145
1145
|
|
|
1146
1146
|
## To whom do I talk to? ##
|
|
1147
1147
|
|
|
1148
|
-
|
|
1149
|
-
* Repo owner : [me@nunobrum.com](mailto:me@nunobrum.com)
|
|
1150
|
-
* Alternative contact : [nuno.brum@gmail.com](mailto:nuno.brum@gmail.com)
|
|
1148
|
+
For support and improvement requests please open an Issue in [GitHub spicelib issues](https://github.com/nunobrum/spicelib/issues)
|
|
1151
1149
|
|
|
1152
1150
|
## History ##
|
|
1151
|
+
* Version 5.4.2
|
|
1152
|
+
* Imported changes from spicelib 1.4.1 (Summary)
|
|
1153
|
+
* Added `get_all_parameter_names()` function to all editors (#159)
|
|
1154
|
+
* Compatibility with LTspice 24+
|
|
1155
|
+
* Fixed Problem with .PARAM regex.
|
|
1156
|
+
* Documenting the user library paths
|
|
1157
|
+
* AscEditor: Adding support to DATAFLAG
|
|
1158
|
+
* LTSteps: Supporting new LTspice data export format
|
|
1159
|
+
* Version 5.4.1
|
|
1160
|
+
* Fixing Issue #146
|
|
1161
|
+
* Version 5.4.0
|
|
1162
|
+
* Adding possibility of manipulating parameters on sub-circuits
|
|
1163
|
+
* Supporting subcircuit names with dots.
|
|
1164
|
+
* Overall documentation improvements (thanks @hb020)
|
|
1165
|
+
* Major improvement in Documentation
|
|
1166
|
+
* Introduced a read-only property that blocks libraries from being updated.
|
|
1167
|
+
* Support for LTspice log files with the option : expanded netlist
|
|
1168
|
+
* Supporting library symbols using BLOCK primitive
|
|
1169
|
+
* Improved unittest on the .ASC hierarchical design
|
|
1170
|
+
* SimRunner simulation iterator only returns successful simulations in order to simplify error management
|
|
1171
|
+
* In QschEditor, the replacement of unique dot instructions (ex: .TRAN .AC .NOISE) is only done if the existing instruction is not commented.
|
|
1172
|
+
* RunTask.get_results() now returns None if a callback function is provided and the simulation has failed.
|
|
1173
|
+
* BigFix: Inclusion of encrypted libraries would crash
|
|
1174
|
+
* Bugfix: Prefix were case sensitive in SpiceEditor
|
|
1175
|
+
* Bugfix: Parsing netlists with extensions other than .net didn't work properly
|
|
1153
1176
|
* Version 5.3.2
|
|
1154
1177
|
* Correction on the readthedocs webhook configuration
|
|
1155
1178
|
* Alignement with the spicelib 1.2.1
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
*(Deprecated)*
|
|
4
|
+
Supporting Legacy import clauses. This will disappear in future versions
|
|
5
|
+
"""
|
|
6
|
+
print("Deprecation Warning! This will no longer be supported in future versions.\n"
|
|
7
|
+
"Use 'from PyLTSpice import SimCommander' for a direct import of the LTSpice/NGSpice batch run class.")
|
|
8
|
+
|
|
9
|
+
from .sim.sim_batch import SimCommander
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
*(Deprecated)*
|
|
4
|
+
Supporting Legacy import clauses. This will disappear in future versions
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
print("Deprecation Warning! This will no longer be supported in future versions.\n"
|
|
8
|
+
"Use 'from PyLTSpice import RawWRead' for a direct import of the Raw Reading class")
|
|
9
|
+
|
|
10
|
+
from .raw.raw_read import *
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
*(Deprecated)*
|
|
4
|
+
Supporting Legacy import clauses. This will disappear in future versions
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
print("Deprecation Warning! This will no longer be supported in future versions.\n"
|
|
8
|
+
"Use 'from PyLTSpice import RawWRead' for a direct import of the Raw Write class")
|
|
9
|
+
|
|
10
|
+
from .raw.raw_write import *
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
|
|
4
|
+
# -------------------------------------------------------------------------------
|
|
5
|
+
# ____ _ _____ ____ _
|
|
6
|
+
# | _ \ _ _| | |_ _/ ___| _ __ (_) ___ ___
|
|
7
|
+
# | |_) | | | | | | | \___ \| '_ \| |/ __/ _ \
|
|
8
|
+
# | __/| |_| | |___| | ___) | |_) | | (_| __/
|
|
9
|
+
# |_| \__, |_____|_| |____/| .__/|_|\___\___|
|
|
10
|
+
# |___/ |_|
|
|
11
|
+
#
|
|
12
|
+
# Name: sim_client.py
|
|
13
|
+
# Purpose: Tool used to launch LTSpice simulation in batch mode.
|
|
14
|
+
#
|
|
15
|
+
# Author: Nuno Brum (nuno.brum@gmail.com)
|
|
16
|
+
#
|
|
17
|
+
# Created: 23-02-2023
|
|
18
|
+
# Licence: refer to the LICENSE file
|
|
19
|
+
# -------------------------------------------------------------------------------
|
|
20
|
+
import os.path
|
|
21
|
+
import zipfile
|
|
22
|
+
import xmlrpc.client
|
|
23
|
+
import io
|
|
24
|
+
import pathlib
|
|
25
|
+
import time
|
|
26
|
+
from collections import OrderedDict
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
import logging
|
|
29
|
+
_logger = logging.getLogger("PyLTSpice.SimClient")
|
|
30
|
+
|
|
31
|
+
class SimClientInvalidRunId(LookupError):
|
|
32
|
+
"""Raised when asking for a run_no that doesn't exist"""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class JobInformation:
|
|
37
|
+
"""Contains information about pending simulation jobs"""
|
|
38
|
+
run_number: int # The run id that is returned by the Server and which identifies the server
|
|
39
|
+
file_dir : pathlib.Path
|
|
40
|
+
|
|
41
|
+
# class RunIterator(object):
|
|
42
|
+
#
|
|
43
|
+
# def __init__(self, client, timeout):
|
|
44
|
+
# self.client = client
|
|
45
|
+
# self.timeout = timeout
|
|
46
|
+
#
|
|
47
|
+
# def __iter__(self):
|
|
48
|
+
# return self
|
|
49
|
+
#
|
|
50
|
+
# def __next__(self):
|
|
51
|
+
# return self.client.__next__()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SimClient(object):
|
|
55
|
+
"""
|
|
56
|
+
Class used for launching simulations in a Spice Simulation Server.
|
|
57
|
+
A Spice Simulation Server is a machine running a script with an active SimServer object.
|
|
58
|
+
|
|
59
|
+
This class only implement basic level handshaking with a single simulation Server.
|
|
60
|
+
Upon instance, it will establish a connection with Simulation Server. This connection is kept
|
|
61
|
+
alive during the whole live of this object.
|
|
62
|
+
|
|
63
|
+
The run() method will transfer the netlist for the server, execute a simulation and transfer the simulation results
|
|
64
|
+
back to the client.
|
|
65
|
+
|
|
66
|
+
Data is returned from the server inside a zipfie which is copied into the directory defined when the job was
|
|
67
|
+
created, /i.e./ run() method called.
|
|
68
|
+
|
|
69
|
+
Two lists are kept by this class:
|
|
70
|
+
|
|
71
|
+
* A list of started jobs (started_jobs) and,
|
|
72
|
+
|
|
73
|
+
* a list with finished jobs on the server, but, which haven't been yet transferred to the client (stored_jobs).
|
|
74
|
+
|
|
75
|
+
This distinction is important because the data is erased on the server side when the data is transferred.
|
|
76
|
+
|
|
77
|
+
This class implements an iterator that is to be used for retrieving the job. See the example below.
|
|
78
|
+
The iterator polls the server with a time interval defined by the attribute ``minimum_time_between_server_calls``.
|
|
79
|
+
This attribute is set to 0.2 seconds by default, but it can be overriden.
|
|
80
|
+
|
|
81
|
+
Usage:
|
|
82
|
+
|
|
83
|
+
.. code-block:: python
|
|
84
|
+
|
|
85
|
+
import zipfile
|
|
86
|
+
from PySpice.sim.sim_client import SimClient
|
|
87
|
+
|
|
88
|
+
server = SimClient('http://localhost', 9000) # Use another computer address.
|
|
89
|
+
print(server.session_id)
|
|
90
|
+
runid = server.run("../../tests/testfile.net")
|
|
91
|
+
print("Got Job id", runid)
|
|
92
|
+
|
|
93
|
+
for runid in server: # may not arrive in the same order as runids were launched
|
|
94
|
+
zip_filename = server.get_runno_data(runid)
|
|
95
|
+
print(f"Received {zip_filename} from runid {runid}")
|
|
96
|
+
|
|
97
|
+
with zipfile.ZipFile(zip_filename, 'r') as zipf: # Extract the contents of the zip file
|
|
98
|
+
print(zipf.namelist()) # Debug printing the contents of the zip file
|
|
99
|
+
zipf.extract(zipf.namelist()[0]) # Normally the raw file comes first
|
|
100
|
+
|
|
101
|
+
NOTE: More elaborate algorithms such as managing multiple servers will be done on another class.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, host_address, port):
|
|
105
|
+
self.server = xmlrpc.client.ServerProxy(f'{host_address}:{port}')
|
|
106
|
+
# print(self.server.system.listMethods())
|
|
107
|
+
self.session_id = self.server.start_session()
|
|
108
|
+
_logger.info("started ", self.session_id)
|
|
109
|
+
self.started_jobs = OrderedDict() # This list keeps track of started jobs on the server
|
|
110
|
+
self.stored_jobs = OrderedDict() # This list keeps track of finished simulations that haven't yet been transferred.
|
|
111
|
+
self.completed_jobs = 0
|
|
112
|
+
self.minimum_time_between_server_calls = 0.2 # Minimum time between server calls
|
|
113
|
+
self._last_server_call = time.clock()
|
|
114
|
+
|
|
115
|
+
def __del__(self):
|
|
116
|
+
_logger.info("closing session ", self.session_id)
|
|
117
|
+
self.server.close_session(self.session_id)
|
|
118
|
+
|
|
119
|
+
def run(self, circuit):
|
|
120
|
+
"""
|
|
121
|
+
Sends the netlist identified with the argument "circuit" to the server, and it receives a run identifier
|
|
122
|
+
(runno). Since the server can receive requests from different machines, this identifier is not guaranteed to be
|
|
123
|
+
sequential.
|
|
124
|
+
|
|
125
|
+
:param circuit: path to the netlist file containing the simulation directives.
|
|
126
|
+
:type circuit: pathlib.Path or str
|
|
127
|
+
:returns: identifier on the server of the simulation.
|
|
128
|
+
:rtype: int
|
|
129
|
+
"""
|
|
130
|
+
circuit_path = pathlib.Path(circuit)
|
|
131
|
+
circuit_name = circuit_path.name
|
|
132
|
+
if os.path.exists(circuit):
|
|
133
|
+
# Create a buffer to store the zip file in memory
|
|
134
|
+
zip_buffer = io.BytesIO()
|
|
135
|
+
|
|
136
|
+
# Create the zip file in memory
|
|
137
|
+
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
|
138
|
+
zip_file.write(circuit, circuit_name) # Makes sure it writes it to the root of the zipfile
|
|
139
|
+
|
|
140
|
+
# Reset the buffer position to the start
|
|
141
|
+
zip_buffer.seek(0)
|
|
142
|
+
|
|
143
|
+
# Read the zip file from the buffer and send it to the server
|
|
144
|
+
zip_data = zip_buffer.read()
|
|
145
|
+
|
|
146
|
+
run_id = self.server.run(self.session_id, circuit_name, zip_data)
|
|
147
|
+
job_info = JobInformation(run_number=run_id, file_dir=circuit_path.parent)
|
|
148
|
+
self.started_jobs[job_info] = job_info
|
|
149
|
+
return run_id
|
|
150
|
+
else:
|
|
151
|
+
_logger.error(f"Circuit {circuit} doesn't exit")
|
|
152
|
+
return -1
|
|
153
|
+
|
|
154
|
+
def get_runno_data(self, runno) -> str:
|
|
155
|
+
"""
|
|
156
|
+
Returns the simulation output data inside a zip file name.
|
|
157
|
+
|
|
158
|
+
:rtype: str
|
|
159
|
+
"""
|
|
160
|
+
if runno not in self.stored_jobs:
|
|
161
|
+
raise SimClientInvalidRunId(f"Invalid Job id {runno}")
|
|
162
|
+
|
|
163
|
+
zip_filename, zipdata = self.server.get_files(self.session_id, runno)
|
|
164
|
+
job = self.stored_jobs.pop(runno) # Removes it from stored jobs
|
|
165
|
+
self.completed_jobs += 1
|
|
166
|
+
if zip_filename != '':
|
|
167
|
+
store_path = job.file_dir
|
|
168
|
+
with open(store_path / zip_filename, 'wb') as f:
|
|
169
|
+
f.write(zipdata.data)
|
|
170
|
+
return zip_filename
|
|
171
|
+
|
|
172
|
+
def __iter__(self):
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def __next__(self):
|
|
176
|
+
while len(self.started_jobs) > 0:
|
|
177
|
+
status = self.server.status(self.session_id)
|
|
178
|
+
if len(status) > 0:
|
|
179
|
+
runno = status.pop(0)
|
|
180
|
+
self.stored_jobs[runno] = self.started_jobs.pop(runno) # Job is taken out of the started jobs list and
|
|
181
|
+
# is added to the stored jobs
|
|
182
|
+
return runno
|
|
183
|
+
else:
|
|
184
|
+
now = time.clock()
|
|
185
|
+
delta = self.minimum_time_between_server_calls - (now - self._last_server_call)
|
|
186
|
+
if delta > 0:
|
|
187
|
+
time.sleep(delta) # Go asleep for a sec
|
|
188
|
+
self._last_server_call = now
|
|
189
|
+
|
|
190
|
+
# when there are no pending jobs left, exit the iterator
|
|
191
|
+
raise StopIteration
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
|
|
196
|
+
server = SimClient('http://localhost', 9000)
|
|
197
|
+
print(server.session_id)
|
|
198
|
+
runid = server.run("../../tests/testfile.net")
|
|
199
|
+
print("Got Job id", runid)
|
|
200
|
+
for runid in server: # Ma
|
|
201
|
+
zip_filename = server.get_runno_data(runid)
|
|
202
|
+
print(f"Received {zip_filename} from runid {runid}")
|
|
203
|
+
with zipfile.ZipFile(zip_filename, 'r') as zipf: # Extract the contents of the zip file
|
|
204
|
+
print(zipf.namelist()) # Debug printing the contents of the zip file
|
|
205
|
+
zipf.extract(zipf.namelist()[0]) # Normally the raw file comes first
|
|
206
|
+
|
|
207
|
+
print("Finished")
|
|
208
|
+
server.server.stop_server() # This will terminate the server
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
|
|
4
|
+
# -------------------------------------------------------------------------------
|
|
5
|
+
# ____ _ _____ ____ _
|
|
6
|
+
# | _ \ _ _| | |_ _/ ___| _ __ (_) ___ ___
|
|
7
|
+
# | |_) | | | | | | | \___ \| '_ \| |/ __/ _ \
|
|
8
|
+
# | __/| |_| | |___| | ___) | |_) | | (_| __/
|
|
9
|
+
# |_| \__, |_____|_| |____/| .__/|_|\___\___|
|
|
10
|
+
# |___/ |_|
|
|
11
|
+
#
|
|
12
|
+
# Name: sim_server.py
|
|
13
|
+
# Purpose: Tool used to launch LTSpice simulation in batch mode.
|
|
14
|
+
#
|
|
15
|
+
# Author: Nuno Brum (nuno.brum@gmail.com)
|
|
16
|
+
#
|
|
17
|
+
# Created: 23-02-2023
|
|
18
|
+
# Licence: refer to the LICENSE file
|
|
19
|
+
# -------------------------------------------------------------------------------
|
|
20
|
+
from typing import Tuple
|
|
21
|
+
from xmlrpc.client import Binary
|
|
22
|
+
from xmlrpc.server import SimpleXMLRPCServer
|
|
23
|
+
import logging
|
|
24
|
+
_logger = logging.getLogger("PyLTSpice.SimServer")
|
|
25
|
+
|
|
26
|
+
import threading
|
|
27
|
+
import zipfile
|
|
28
|
+
import io
|
|
29
|
+
from PyLTSpice.client_server.srv_sim_runner import ServerSimRunner
|
|
30
|
+
import uuid
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SimServer():
|
|
34
|
+
|
|
35
|
+
def __init__(self, simulator, parallel_sims=4, output_folder='./temp1', port=9000):
|
|
36
|
+
self.output_folder = output_folder
|
|
37
|
+
self.simulation_manager = ServerSimRunner(parallel_sims=parallel_sims, timeout=5 * 60, verbose=True,
|
|
38
|
+
output_folder=output_folder, simulator=simulator)
|
|
39
|
+
self.server = SimpleXMLRPCServer(
|
|
40
|
+
('localhost', port),
|
|
41
|
+
# requestHandler=RequestHandler
|
|
42
|
+
)
|
|
43
|
+
self.server.register_introspection_functions()
|
|
44
|
+
self.server.register_instance(self)
|
|
45
|
+
self.sessions = {} # this will contain the session_id ids hashing their respective list of sim_tasks
|
|
46
|
+
self.simulation_manager.start()
|
|
47
|
+
self.server_thread = threading.Thread(target=self.server.serve_forever, name="ServerThread")
|
|
48
|
+
self.server_thread.start()
|
|
49
|
+
|
|
50
|
+
def run(self, session_id, circuit_name, zip_data):
|
|
51
|
+
_logger.info("Run ", session_id, circuit_name)
|
|
52
|
+
if session_id not in self.sessions:
|
|
53
|
+
return -1 # This indicates that no job is started
|
|
54
|
+
# Create a buffer from the zip data
|
|
55
|
+
zip_buffer = io.BytesIO(zip_data.data)
|
|
56
|
+
_logger.debug("Created the buffer")
|
|
57
|
+
# Extract the contents of the zip file
|
|
58
|
+
with zipfile.ZipFile(zip_buffer, 'r') as zip_file:
|
|
59
|
+
_logger.debug(zip_file.namelist())
|
|
60
|
+
zip_file.extract(circuit_name, self.output_folder)
|
|
61
|
+
|
|
62
|
+
_logger.info(f"Running simulation of {circuit_name}")
|
|
63
|
+
runno = self.simulation_manager.add_simulation(circuit_name)
|
|
64
|
+
if runno != -1:
|
|
65
|
+
self.sessions[session_id].append(runno)
|
|
66
|
+
return runno
|
|
67
|
+
|
|
68
|
+
def start_session(self):
|
|
69
|
+
"""Returns an unique key that represents the session. It will be later used to sort the sim_tasks belonging
|
|
70
|
+
to the session."""
|
|
71
|
+
session_id = str(uuid.uuid4()) # Needs to be a string, otherwise the rpc client can't handle it
|
|
72
|
+
_logger.info("Starting session ", session_id)
|
|
73
|
+
self.sessions[session_id] = []
|
|
74
|
+
return session_id
|
|
75
|
+
|
|
76
|
+
def status(self, session_id):
|
|
77
|
+
"""
|
|
78
|
+
Returns a dictionary with task information. The key for the dictionary is the simulation identifier returned
|
|
79
|
+
by the simulation start command. The value associated with each simulation identifier is another dictionary
|
|
80
|
+
containing the following keys:
|
|
81
|
+
|
|
82
|
+
* 'completed' - whether the simulation is already finished
|
|
83
|
+
|
|
84
|
+
* 'start' - time when the simulation was started
|
|
85
|
+
|
|
86
|
+
* 'stop' - server time
|
|
87
|
+
"""
|
|
88
|
+
_logger.debug("collecting status for ", session_id)
|
|
89
|
+
ret = []
|
|
90
|
+
for task_info in self.simulation_manager.completed_tasks:
|
|
91
|
+
_logger.debug(task_info)
|
|
92
|
+
runno = task_info['runno']
|
|
93
|
+
if runno in self.sessions[session_id]:
|
|
94
|
+
ret.append(runno) # transfers the dictionary from the simulation_manager completed task
|
|
95
|
+
# to the return dictionary
|
|
96
|
+
_logger.debug("returning status", ret)
|
|
97
|
+
return ret
|
|
98
|
+
|
|
99
|
+
def get_files(self, session_id, runno) -> Tuple[str, Binary]:
|
|
100
|
+
if runno in self.sessions[session_id]:
|
|
101
|
+
|
|
102
|
+
for task_info in self.simulation_manager.completed_tasks:
|
|
103
|
+
if runno == task_info['runno']:
|
|
104
|
+
# Create a buffer to store the zip file in memory
|
|
105
|
+
zip_file = task_info['zipfile']
|
|
106
|
+
zip = zip_file.open('rb')
|
|
107
|
+
# Read the zip file from the buffer and send it to the server
|
|
108
|
+
zip_data = zip.read()
|
|
109
|
+
zip.close()
|
|
110
|
+
self.simulation_manager.erase_files_of_runno(runno)
|
|
111
|
+
return zip_file.name, Binary(zip_data)
|
|
112
|
+
|
|
113
|
+
return "", Binary(b'') # Returns and empty data
|
|
114
|
+
|
|
115
|
+
def close_session(self, session_id):
|
|
116
|
+
"""Cleans all the pending sim_tasks with """
|
|
117
|
+
for runno in self.sessions[session_id]:
|
|
118
|
+
self.simulation_manager.erase_files_of_runno(runno)
|
|
119
|
+
return True # Needs to return always something. None is not supported
|
|
120
|
+
|
|
121
|
+
def stop_server(self):
|
|
122
|
+
_logger.info("stopping...ServerInterface")
|
|
123
|
+
self.simulation_manager.stop()
|
|
124
|
+
self.server.shutdown()
|
|
125
|
+
_logger.info("stopped...ServerInterface")
|
|
126
|
+
return True # Needs to return always something. None is not supported
|
|
127
|
+
|
|
128
|
+
def running(self):
|
|
129
|
+
return self.simulation_manager.running()
|
|
130
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
|
|
4
|
+
# -------------------------------------------------------------------------------
|
|
5
|
+
# ____ _ _____ ____ _
|
|
6
|
+
# | _ \ _ _| | |_ _/ ___| _ __ (_) ___ ___
|
|
7
|
+
# | |_) | | | | | | | \___ \| '_ \| |/ __/ _ \
|
|
8
|
+
# | __/| |_| | |___| | ___) | |_) | | (_| __/
|
|
9
|
+
# |_| \__, |_____|_| |____/| .__/|_|\___\___|
|
|
10
|
+
# |___/ |_|
|
|
11
|
+
#
|
|
12
|
+
# Name: srv_sim_runner.py
|
|
13
|
+
# Purpose: Manager of the simulation sim_tasks on the server side
|
|
14
|
+
#
|
|
15
|
+
# Author: Nuno Brum (nuno.brum@gmail.com)
|
|
16
|
+
#
|
|
17
|
+
# Created: 23-02-2023
|
|
18
|
+
# Licence: refer to the LICENSE file
|
|
19
|
+
# -------------------------------------------------------------------------------
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from typing import Any, Callable, Union
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
import zipfile
|
|
25
|
+
import logging
|
|
26
|
+
_logger = logging.getLogger("PyLTSpice.ServerSimRunner")
|
|
27
|
+
|
|
28
|
+
from PyLTSpice.sim.sim_runner import SimRunner
|
|
29
|
+
from PyLTSpice.sim.spice_editor import SpiceEditor
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def zip_files(raw_filename: Path, log_filename:Path):
|
|
33
|
+
zip_filename = raw_filename.with_suffix('.zip')
|
|
34
|
+
with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
|
35
|
+
zip_file.write(raw_filename)
|
|
36
|
+
zip_file.write(log_filename)
|
|
37
|
+
return zip_filename
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ServerSimRunner(threading.Thread):
|
|
41
|
+
"""This class maintains updated status of the SimRunner.
|
|
42
|
+
It was decided not to make SimRunner a super class and rather make it manipulate directly the structures of
|
|
43
|
+
SimRunner. The rationale for this, was to avoid confusions between the run() on the Thread class and the
|
|
44
|
+
run on the SimRunner class.
|
|
45
|
+
Making a class derive from two different classes needs to be handled carefully.
|
|
46
|
+
|
|
47
|
+
In consequence of the rationale above, many of the functions that were handled by the SimRunner are overriden
|
|
48
|
+
by this class.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, parallel_sims: int = 4, timeout=None, verbose=True, output_folder: str = None, simulator=None):
|
|
52
|
+
super().__init__(name="SimManager")
|
|
53
|
+
self.runner = SimRunner(parallel_sims, timeout, verbose, output_folder, simulator)
|
|
54
|
+
self.completed_tasks = []
|
|
55
|
+
self._stop = False
|
|
56
|
+
|
|
57
|
+
def run(self) -> None:
|
|
58
|
+
"""This function makes a direct manipulation of the structures of SimRunner. This option is """
|
|
59
|
+
while True:
|
|
60
|
+
i = 0
|
|
61
|
+
while i < len(self.runner.sim_tasks):
|
|
62
|
+
task = self.runner.sim_tasks[i]
|
|
63
|
+
if task.is_alive():
|
|
64
|
+
i += 1
|
|
65
|
+
else:
|
|
66
|
+
zip_filename = task.callback_return
|
|
67
|
+
self.completed_tasks.append({
|
|
68
|
+
'runno': task.runno,
|
|
69
|
+
'retcode': task.retcode,
|
|
70
|
+
'raw': task.raw_file,
|
|
71
|
+
'log': task.log_file,
|
|
72
|
+
'zipfile': zip_filename,
|
|
73
|
+
'start': task.start_time,
|
|
74
|
+
'stop': task.stop_time,
|
|
75
|
+
})
|
|
76
|
+
_logger.debug(task, "is finished")
|
|
77
|
+
del self.runner.sim_tasks[i]
|
|
78
|
+
_logger.debug(self.completed_tasks[-1])
|
|
79
|
+
_logger.debug(len(self.completed_tasks))
|
|
80
|
+
|
|
81
|
+
time.sleep(0.2)
|
|
82
|
+
if self._stop is True:
|
|
83
|
+
break
|
|
84
|
+
self.runner.wait_completion()
|
|
85
|
+
self.runner.file_cleanup()
|
|
86
|
+
|
|
87
|
+
def add_simulation(self, netlist: Union[str, Path, SpiceEditor], *, timeout: float = 600) -> int:
|
|
88
|
+
""""""
|
|
89
|
+
_logger.debug("starting ", netlist)
|
|
90
|
+
task = self.runner.run(netlist, wait_resource=True, timeout=timeout, callback=zip_files)
|
|
91
|
+
if task is None:
|
|
92
|
+
_logger.error("Failed to start task ", netlist)
|
|
93
|
+
return -1
|
|
94
|
+
else:
|
|
95
|
+
_logger.info("Started task ", netlist, " with job_id", task.runno)
|
|
96
|
+
return task.runno
|
|
97
|
+
|
|
98
|
+
def _erase_files_and_info(self, pos):
|
|
99
|
+
task = self.completed_tasks[pos]
|
|
100
|
+
for filename in ('log', 'raw', 'zipfile'):
|
|
101
|
+
f = task[filename]
|
|
102
|
+
if f.exists():
|
|
103
|
+
f.unlink()
|
|
104
|
+
del self.completed_tasks[pos]
|
|
105
|
+
|
|
106
|
+
def erase_files_of_runno(self, runno):
|
|
107
|
+
"""Will delete all files related with a completed task. Will also delete information on the completed_tasks
|
|
108
|
+
attribute."""
|
|
109
|
+
for i, task_info in enumerate(self.completed_tasks):
|
|
110
|
+
if task_info['runno'] == runno:
|
|
111
|
+
self._erase_files_and_info(i)
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
def cleanup_completed(self):
|
|
115
|
+
while len(self.completed_tasks):
|
|
116
|
+
self._erase_files_and_info(0)
|
|
117
|
+
|
|
118
|
+
def stop(self):
|
|
119
|
+
_logger.info("stopping...ServerSimRunner")
|
|
120
|
+
self._stop = True
|
|
121
|
+
|
|
122
|
+
def running(self):
|
|
123
|
+
return self._stop is False
|